From 421b80079732d420e0744b69cfc83f1caec5a0c5 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 14 Oct 2026 23:50:35 +0200 Subject: [PATCH 01/18] Added RetryPolicy class --- .../github/jopenlibs/vault/RetryPolicy.java | 70 +++++++++++++++++++ .../jopenlibs/vault/RetryPolicyTests.java | 53 ++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/main/java/io/github/jopenlibs/vault/RetryPolicy.java create mode 100644 src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java diff --git a/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java new file mode 100644 index 00000000..607dc810 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java @@ -0,0 +1,70 @@ +package io.github.jopenlibs.vault; + +/** + *

An immutable retry setting for API operations: the maximum number of times a failing + * operation is retried, and the time to wait between attempts.

+ * + *

A policy with maxRetries set to 0 disables retries.

+ */ +public final class RetryPolicy { + + private final int maxRetries; + private final int retryIntervalMilliseconds; + + /** + * Creates a retry policy. + * + * @param maxRetries The number of times that a failing API operation will be retried + * @param retryIntervalMilliseconds The number of milliseconds to wait between retries + * @throws IllegalArgumentException If either value is negative + */ + public RetryPolicy(final int maxRetries, final int retryIntervalMilliseconds) { + if (maxRetries < 0) { + throw new IllegalArgumentException("maxRetries must not be negative: " + maxRetries); + } + if (retryIntervalMilliseconds < 0) { + throw new IllegalArgumentException( + "retryIntervalMilliseconds must not be negative: " + retryIntervalMilliseconds); + } + this.maxRetries = maxRetries; + this.retryIntervalMilliseconds = retryIntervalMilliseconds; + } + + /** + * @return The number of times that a failing API operation will be retried + */ + public int getMaxRetries() { + return maxRetries; + } + + /** + * @return The number of milliseconds to wait between retries + */ + public int getRetryIntervalMilliseconds() { + return retryIntervalMilliseconds; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RetryPolicy)) { + return false; + } + final RetryPolicy other = (RetryPolicy) o; + return maxRetries == other.maxRetries + && retryIntervalMilliseconds == other.retryIntervalMilliseconds; + } + + @Override + public int hashCode() { + return 31 * maxRetries + retryIntervalMilliseconds; + } + + @Override + public String toString() { + return "RetryPolicy{maxRetries=" + maxRetries + + ", retryIntervalMilliseconds=" + retryIntervalMilliseconds + "}"; + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java b/src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java new file mode 100644 index 00000000..914d5f51 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/RetryPolicyTests.java @@ -0,0 +1,53 @@ +package io.github.jopenlibs.vault; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +public class RetryPolicyTests { + + @Test + public void testValues() { + final var policy = new RetryPolicy(5, 1000); + assertEquals(5, policy.getMaxRetries()); + assertEquals(1000, policy.getRetryIntervalMilliseconds()); + } + + @Test + public void testZeroValuesAllowed() { + final var policy = new RetryPolicy(0, 0); + assertEquals(0, policy.getMaxRetries()); + assertEquals(0, policy.getRetryIntervalMilliseconds()); + } + + @Test + public void testNegativeMaxRetriesRejected() { + assertThrows(IllegalArgumentException.class, () -> new RetryPolicy(-1, 1000)); + } + + @Test + public void testNegativeIntervalRejected() { + assertThrows(IllegalArgumentException.class, () -> new RetryPolicy(5, -1)); + } + + @Test + public void testEqualsAndHashCode() { + final var policy = new RetryPolicy(5, 1000); + final var same = new RetryPolicy(5, 1000); + assertEquals(policy, policy); + assertEquals(policy, same); + assertEquals(policy.hashCode(), same.hashCode()); + assertNotEquals(policy, new RetryPolicy(4, 1000)); + assertNotEquals(policy, new RetryPolicy(5, 999)); + assertNotEquals(policy, null); + assertNotEquals(policy, "RetryPolicy"); + } + + @Test + public void testToString() { + assertEquals("RetryPolicy{maxRetries=5, retryIntervalMilliseconds=1000}", + new RetryPolicy(5, 1000).toString()); + } +} From 701db2c225b2e2ba16b32ccddfdaa73d09e31001 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 14 Oct 2026 23:53:09 +0200 Subject: [PATCH 02/18] Implement RetryPolicy in Logical operations --- .../github/jopenlibs/vault/api/Logical.java | 4 +- .../jopenlibs/vault/api/OperationsBase.java | 38 +++++++- .../vault/api/OperationsBaseRetryTests.java | 92 +++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/api/Logical.java b/src/main/java/io/github/jopenlibs/vault/api/Logical.java index ca7c91e7..a6710e20 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -164,8 +164,8 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege return new LogicalResponse(restResponse, attempt, logicalOperations.readV2); }, - shouldRetry ? config.getMaxRetries() : 1, - config.getRetryIntervalMilliseconds() + shouldRetry ? getMaxRetries() : 1, + getRetryIntervalMilliseconds() ); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java b/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java index 5a0b9f66..7c5940ed 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java +++ b/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.rest.Rest; @@ -14,12 +15,47 @@ public abstract class OperationsBase { protected final VaultConfig config; + /** + * The retries for this instance, or null to use the retries currently set in + * {@link #config}. + */ + protected final RetryPolicy retryPolicy; + protected OperationsBase(VaultConfig config) { + this(config, null); + } + + /** + * @param config The configuration + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration, read on every + * call + */ + protected OperationsBase(VaultConfig config, RetryPolicy retryPolicy) { this.config = config; + this.retryPolicy = retryPolicy; + } + + /** + * @return The maximum number of retries from the retry policy if set, otherwise the one + * currently set in the configuration + */ + protected int getMaxRetries() { + return retryPolicy != null ? retryPolicy.getMaxRetries() : config.getMaxRetries(); + } + + /** + * @return The interval between retries from the retry policy if set, otherwise the one + * currently set in the configuration + */ + protected int getRetryIntervalMilliseconds() { + return retryPolicy != null + ? retryPolicy.getRetryIntervalMilliseconds() + : config.getRetryIntervalMilliseconds(); } protected T retry(final EndpointOperation op) throws VaultException { - return retry(op, config.getMaxRetries(), config.getRetryIntervalMilliseconds()); + return retry(op, getMaxRetries(), getRetryIntervalMilliseconds()); } /** diff --git a/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java b/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java index e339f8a8..3dbb3a64 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java @@ -1,5 +1,8 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.rest.Rest; import io.github.jopenlibs.vault.rest.RestException; @@ -10,6 +13,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -107,6 +111,94 @@ public void testStopsRetryingWhenInterrupted() { assertTrue(Thread.currentThread().isInterrupted()); } + @Test + public void testNullPolicyUsesConfigRetries() throws Exception { + final var config = configWithRetries(2, 1); + final var operations = new TestOperations(config); + assertNull(operations.retryPolicy); + assertEquals(2, operations.getMaxRetries()); + assertEquals(1, operations.getRetryIntervalMilliseconds()); + assertEquals(3, operations.countAttempts()); + } + + @Test + public void testNullPolicyReadsConfigOnEveryCall() throws Exception { + final var config = configWithRetries(1, 1); + final var operations = new TestOperations(config, null); + assertEquals(2, operations.countAttempts()); + + Vault.create(config).withRetries(3, 1); + assertEquals(3, operations.getMaxRetries()); + assertEquals(4, operations.countAttempts()); + } + + @Test + public void testNullPolicyKeepsNegativeConfigValues() throws Exception { + final var config = configWithRetries(-1, -5); + final var operations = new TestOperations(config); + assertEquals(-1, operations.getMaxRetries()); + assertEquals(-5, operations.getRetryIntervalMilliseconds()); + assertEquals(1, operations.countAttempts()); + } + + @Test + public void testPolicyOverridesConfigRetries() throws Exception { + final var config = configWithRetries(5, 1000); + final var policy = new RetryPolicy(1, 2); + final var operations = new TestOperations(config, policy); + assertSame(policy, operations.retryPolicy); + assertEquals(1, operations.getMaxRetries()); + assertEquals(2, operations.getRetryIntervalMilliseconds()); + assertEquals(2, operations.countAttempts()); + + Vault.create(config).withRetries(3, 1); + assertEquals(2, operations.countAttempts()); + assertEquals(3, config.getMaxRetries()); + } + + @Test + public void testZeroPolicyDisablesRetries() throws Exception { + final var operations = new TestOperations(configWithRetries(5, 1), new RetryPolicy(0, 0)); + assertEquals(1, operations.countAttempts()); + } + + private static VaultConfig configWithRetries(final int maxRetries, + final int retryIntervalMilliseconds) throws VaultException { + final var config = new VaultConfig().address("http://127.0.0.1:8999") + .token("mock_token").engineVersion(1).build(); + Vault.create(config).withRetries(maxRetries, retryIntervalMilliseconds); + return config; + } + + private static final class TestOperations extends OperationsBase { + + TestOperations(final VaultConfig config) { + super(config); + } + + TestOperations(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); + } + + /** + * @return The number of attempts made for an operation that always fails with a transient + * error + */ + int countAttempts() { + final var calls = new AtomicInteger(); + try { + retry(attempt -> { + calls.incrementAndGet(); + throw new VaultException("unavailable", 503); + }); + fail("Expected VaultException"); + } catch (VaultException e) { + assertEquals(503, e.getHttpStatusCode()); + } + return calls.get(); + } + } + private static RestException connectionRefused() { try { new Rest().url("http://127.0.0.1:1/").connectTimeoutSeconds(1).get(); From 6bb9a40de84ef3c475cff0dd43c38724dc380b88 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 14 Oct 2026 23:56:29 +0200 Subject: [PATCH 03/18] Implement RetryPolicy in all constructor classes --- .../io/github/jopenlibs/vault/api/Auth.java | 29 +++-- .../io/github/jopenlibs/vault/api/Debug.java | 13 ++- .../github/jopenlibs/vault/api/Logical.java | 13 ++- .../vault/api/database/Database.java | 19 +++- .../github/jopenlibs/vault/api/pki/Pki.java | 19 +++- .../jopenlibs/vault/api/sys/Leases.java | 13 ++- .../github/jopenlibs/vault/api/sys/Seal.java | 13 ++- .../github/jopenlibs/vault/api/sys/Sys.java | 21 +++- .../jopenlibs/vault/api/sys/Wrapping.java | 13 ++- .../vault/api/sys/mounts/Mounts.java | 13 ++- .../api/RetryPolicyPropagationTests.java | 105 ++++++++++++++++++ 11 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java diff --git a/src/main/java/io/github/jopenlibs/vault/api/Auth.java b/src/main/java/io/github/jopenlibs/vault/api/Auth.java index 42017d04..55747698 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.VaultImpl; @@ -254,7 +255,17 @@ public String getEntityAlias() { private String nameSpace; public Auth(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Auth(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } @@ -1153,7 +1164,7 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept */ @Deprecated public LogicalResponse lookupWrap() throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().lookupWrap(config.getToken(), false); } @@ -1163,7 +1174,7 @@ public LogicalResponse lookupWrap() throws VaultException { */ @Deprecated public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().lookupWrap(wrappedToken, true); } @@ -1174,7 +1185,7 @@ public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultExcepti @Deprecated public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().lookupWrap(wrappedToken, inBody); } @@ -1220,7 +1231,7 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException { */ @Deprecated public UnwrapResponse unwrap() throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().unwrap(config.getToken(), false); } @@ -1230,7 +1241,7 @@ public UnwrapResponse unwrap() throws VaultException { */ @Deprecated public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().unwrap(wrappedToken, true); } @@ -1240,7 +1251,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { */ @Deprecated public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().unwrap(wrappedToken, inBody); } @@ -1250,7 +1261,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V */ @Deprecated public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().wrap(jsonObject, ttlInSec); } @@ -1260,7 +1271,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault */ @Deprecated public WrapResponse rewrap(final char[] wrappedToken) throws VaultException { - final var sys = new Sys(this.config); + final var sys = new Sys(this.config, this.retryPolicy); return sys.wrapping().rewrap(wrappedToken); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/Debug.java b/src/main/java/io/github/jopenlibs/vault/api/Debug.java index 52832f34..ffe4de32 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Debug.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Debug.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.HealthResponse; @@ -21,7 +22,17 @@ public class Debug extends OperationsBase { private String nameSpace; public Debug(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Debug(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/Logical.java b/src/main/java/io/github/jopenlibs/vault/api/Logical.java index a6710e20..9d3fbbcb 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.json.Json; @@ -37,7 +38,17 @@ public class Logical extends OperationsBase { public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, listSubKeys, readV1, readV2, writeV1, writeV2, unDelete, mount} public Logical(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Logical(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/database/Database.java b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java index b58fee17..4f51a72d 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/database/Database.java +++ b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.database; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -49,7 +50,23 @@ public Database(final VaultConfig config) { * /v1/ prefix (e.g. "root-ca") */ public Database(final VaultConfig config, final String mountPath) { - super(config); + this(config, mountPath, null); + } + + /** + * Constructor for use when the Database backend is mounted on some non-default custom path, with + * a retry policy. + * + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param mountPath The path on which your Vault Database backend is mounted, without the + * /v1/ prefix (e.g. "db123") + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Database(final VaultConfig config, final String mountPath, + final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.mountPath = mountPath; this.nameSpace = this.config.getNameSpace(); diff --git a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java index 1a3148e8..9fb0463e 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java +++ b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.pki; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -50,7 +51,23 @@ public Pki(final VaultConfig config) { * /v1/ prefix (e.g. "root-ca") */ public Pki(final VaultConfig config, final String mountPath) { - super(config); + this(config, mountPath, null); + } + + /** + * Constructor for use when the PKI backend is mounted on some non-default custom path, with + * a retry policy. + * + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param mountPath The path on which your Vault PKI backend is mounted, without the + * /v1/ prefix (e.g. "root-ca") + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Pki(final VaultConfig config, final String mountPath, + final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.mountPath = mountPath; this.nameSpace = this.config.getNameSpace(); diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java index 10938922..8e06d2dd 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -23,7 +24,17 @@ public class Leases extends OperationsBase { private String nameSpace; public Leases(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Leases(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java index d4bd8e02..976305da 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -23,7 +24,17 @@ public class Seal extends OperationsBase { private String nameSpace; public Seal(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Seal(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java index 98011247..f5d3dc53 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.api.OperationsBase; @@ -19,7 +20,17 @@ public class Sys extends OperationsBase { private String nameSpace; public Sys(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Sys(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } @@ -35,7 +46,7 @@ public Sys withNameSpace(final String nameSpace) { * @return The implementing class for wrapping operations */ public Wrapping wrapping() { - return new Wrapping(this.config); + return new Wrapping(this.config, this.retryPolicy); } /** @@ -44,7 +55,7 @@ public Wrapping wrapping() { * @return The implementing class for Vault's seal operations (e.g. seal, unseal, sealStatus). */ public Seal seal() { - return new Seal(this.config); + return new Seal(this.config, this.retryPolicy); } /** @@ -54,7 +65,7 @@ public Seal seal() { * @return the implementing class for Vault's sys mounts operations */ public Mounts mounts() { - return new Mounts(this.config); + return new Mounts(this.config, this.retryPolicy); } /** @@ -64,6 +75,6 @@ public Mounts mounts() { * @return The implementing class for Vault's lease operations */ public Leases leases() { - return new Leases(this.config); + return new Leases(this.config, this.retryPolicy); } } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java index b8823ff9..bb88489c 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.Logical; @@ -27,7 +28,17 @@ public class Wrapping extends OperationsBase { private String nameSpace; public Wrapping(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Wrapping(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); this.nameSpace = this.config.getNameSpace(); } diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java index 42b655bd..fa2862b6 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java @@ -1,5 +1,6 @@ package io.github.jopenlibs.vault.api.sys.mounts; +import io.github.jopenlibs.vault.RetryPolicy; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; @@ -17,7 +18,17 @@ public class Mounts extends OperationsBase { public Mounts(final VaultConfig config) { - super(config); + this(config, null); + } + + /** + * @param config A container for the configuration settings needed to initialize a + * Vault driver instance + * @param retryPolicy The retries for this instance, overriding those set in the configuration, + * or null to use the retries currently set in the configuration + */ + public Mounts(final VaultConfig config, final RetryPolicy retryPolicy) { + super(config, retryPolicy); } /** diff --git a/src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java b/src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java new file mode 100644 index 00000000..e621cc03 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/api/RetryPolicyPropagationTests.java @@ -0,0 +1,105 @@ +package io.github.jopenlibs.vault.api; + +import io.github.jopenlibs.vault.RetryPolicy; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.api.database.Database; +import io.github.jopenlibs.vault.api.pki.Pki; +import io.github.jopenlibs.vault.api.sys.Leases; +import io.github.jopenlibs.vault.api.sys.Seal; +import io.github.jopenlibs.vault.api.sys.Sys; +import io.github.jopenlibs.vault.api.sys.Wrapping; +import io.github.jopenlibs.vault.api.sys.mounts.Mounts; +import io.github.jopenlibs.vault.mock.RetriesMockVault; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * Tests that the operation classes keep the {@link RetryPolicy} they are given, and pass it to the + * operation classes they create. + */ +public class RetryPolicyPropagationTests { + + private static final RetryPolicy POLICY = new RetryPolicy(3, 10); + + @Test + public void testConstructorsKeepPolicy() throws VaultException { + final var config = config(); + assertSame(POLICY, new Logical(config, POLICY).retryPolicy); + assertSame(POLICY, new Auth(config, POLICY).retryPolicy); + assertSame(POLICY, new Debug(config, POLICY).retryPolicy); + assertSame(POLICY, new Sys(config, POLICY).retryPolicy); + assertSame(POLICY, new Leases(config, POLICY).retryPolicy); + assertSame(POLICY, new Seal(config, POLICY).retryPolicy); + assertSame(POLICY, new Wrapping(config, POLICY).retryPolicy); + assertSame(POLICY, new Mounts(config, POLICY).retryPolicy); + assertSame(POLICY, new Pki(config, "pki", POLICY).retryPolicy); + assertSame(POLICY, new Database(config, "database", POLICY).retryPolicy); + } + + @Test + public void testConfigOnlyConstructorsHaveNoPolicy() throws VaultException { + final var config = config(); + assertNull(new Logical(config).retryPolicy); + assertNull(new Auth(config).retryPolicy); + assertNull(new Debug(config).retryPolicy); + assertNull(new Sys(config).retryPolicy); + assertNull(new Leases(config).retryPolicy); + assertNull(new Seal(config).retryPolicy); + assertNull(new Wrapping(config).retryPolicy); + assertNull(new Mounts(config).retryPolicy); + assertNull(new Pki(config).retryPolicy); + assertNull(new Pki(config, "root-ca").retryPolicy); + assertNull(new Database(config).retryPolicy); + assertNull(new Database(config, "db123").retryPolicy); + } + + @Test + public void testPkiAndDatabaseKeepMountPathConstructorWithNull() throws VaultException { + final var config = config(); + // Must still compile: a (VaultConfig, RetryPolicy) overload would make these ambiguous + assertNull(new Pki(config, null).retryPolicy); + assertNull(new Database(config, null).retryPolicy); + } + + @Test + public void testSysPassesPolicy() throws VaultException { + final var sys = new Sys(config(), POLICY); + assertSame(POLICY, sys.wrapping().retryPolicy); + assertSame(POLICY, sys.seal().retryPolicy); + assertSame(POLICY, sys.mounts().retryPolicy); + assertSame(POLICY, sys.leases().retryPolicy); + } + + @Test + public void testSysWithoutPolicyPassesNull() throws VaultException { + final var sys = new Sys(config()); + assertNull(sys.wrapping().retryPolicy); + assertNull(sys.seal().retryPolicy); + assertNull(sys.mounts().retryPolicy); + assertNull(sys.leases().retryPolicy); + } + + @Test + public void testAuthPassesPolicyToSys() throws Exception { + // The config allows no retries, so the call only succeeds if Auth passes its policy on + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(2, 200, "{\"data\":{\"token\":\"mock\"}}")); + server.start(); + try { + final var response = new Auth(config(), POLICY).lookupWrap(); + assertEquals(2, response.getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + private static VaultConfig config() throws VaultException { + return new VaultConfig().address("http://127.0.0.1:8999").token("mock_token") + .engineVersion(1).build(); + } +} From b424b9b9314a651f47b3e305fec33a93e8055d6c Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 15 Oct 2026 00:01:49 +0200 Subject: [PATCH 04/18] Added new retrying() method in interface --- .../github/jopenlibs/vault/RetryPolicy.java | 2 + .../java/io/github/jopenlibs/vault/Vault.java | 31 ++++ .../io/github/jopenlibs/vault/VaultImpl.java | 59 ++++++-- .../io/github/jopenlibs/vault/RetryTests.java | 139 ++++++++++++++++++ 4 files changed, 220 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java index 607dc810..06d9b09c 100644 --- a/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java +++ b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java @@ -5,6 +5,8 @@ * operation is retried, and the time to wait between attempts.

* *

A policy with maxRetries set to 0 disables retries.

+ * + * @see Vault#retrying(int, int) */ public final class RetryPolicy { diff --git a/src/main/java/io/github/jopenlibs/vault/Vault.java b/src/main/java/io/github/jopenlibs/vault/Vault.java index dffb117f..89e15a7d 100644 --- a/src/main/java/io/github/jopenlibs/vault/Vault.java +++ b/src/main/java/io/github/jopenlibs/vault/Vault.java @@ -61,6 +61,37 @@ static Vault create(final VaultConfig vaultConfig, final Boolean useSecretsEngin */ Vault withRetries(final int maxRetries, final int retryIntervalMilliseconds); + /** + *

Returns a new Vault whose API operations are retried with the given rules, + * overriding the retries set in the configuration. This instance, and its configuration, are + * left unchanged:

+ * + *
+ *
{@code
+     * final LogicalResponse response = vault.retrying(5, 1000).logical().read("secret/hello");
+     * }
+ *
+ * + *

The returned instance shares the configuration of this one, so later changes to it (e.g. + * a new token) apply to both.

+ * + *

The default implementation throws UnsupportedOperationException, so that + * existing implementations of this interface keep compiling.

+ * + * @param maxRetries The number of times that API operations will be retried when a failure + * occurs + * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in + * between retries + * @return A new Vault using the given retries + * @throws IllegalArgumentException If either value is negative + * @throws UnsupportedOperationException If this implementation does not support it + * @see RetryPolicy + */ + default Vault retrying(final int maxRetries, final int retryIntervalMilliseconds) { + throw new UnsupportedOperationException( + getClass().getName() + " does not support retrying()"); + } + /** * Returns the implementing class for Vault's core/logical operations (e.g. read, write). * diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 04b81768..daa15a7d 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -58,6 +58,11 @@ public class VaultImpl implements Vault { private final VaultConfig vaultConfig; + /** + * The retries for the operations of this instance, or null to use the retries + * currently set in {@link #vaultConfig}. + */ + private final RetryPolicy retryPolicy; private final Logger logger = Logger.getLogger(VaultImpl.class.getCanonicalName()); /** @@ -69,6 +74,7 @@ public class VaultImpl implements Vault { */ public VaultImpl(final VaultConfig vaultConfig) { this.vaultConfig = vaultConfig; + this.retryPolicy = null; logNameSpace(); if (this.vaultConfig.getSecretsEnginePathMap().isEmpty() && this.vaultConfig.getGlobalEngineVersion() == null) { @@ -95,6 +101,7 @@ public VaultImpl(final VaultConfig vaultConfig, final Integer engineVersion) { } vaultConfig.setEngineVersion(engineVersion); this.vaultConfig = vaultConfig; + this.retryPolicy = null; logNameSpace(); } @@ -115,6 +122,7 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa final Integer globalFallbackVersion) throws VaultException { this.vaultConfig = vaultConfig; + this.retryPolicy = null; logNameSpace(); this.vaultConfig.setEngineVersion(globalFallbackVersion); if (Boolean.TRUE.equals(useSecretsEnginePathMap) && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) { @@ -139,6 +147,15 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa } } + /** + * Used by {@link #retrying(int, int)}: shares the configuration of an existing instance, which + * has already been set up, so the engine version setup is not run again. + */ + private VaultImpl(final VaultConfig vaultConfig, final RetryPolicy retryPolicy) { + this.vaultConfig = vaultConfig; + this.retryPolicy = retryPolicy; + } + private void logNameSpace() { final String nameSpace = this.vaultConfig.getNameSpace(); if (nameSpace != null && !nameSpace.isEmpty()) { @@ -165,6 +182,26 @@ public VaultImpl withRetries(final int maxRetries, final int retryIntervalMillis return this; } + /** + *

Returns a new VaultImpl whose API operations are retried with the given + * rules, overriding the retries set in the configuration. This instance, and its configuration, + * are left unchanged.

+ * + *

The returned instance shares the configuration of this one, so later changes to it (e.g. + * a new token) apply to both.

+ * + * @param maxRetries The number of times that API operations will be retried when a failure + * occurs + * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in + * between retries + * @return A new VaultImpl using the given retries + * @throws IllegalArgumentException If either value is negative + */ + @Override + public VaultImpl retrying(final int maxRetries, final int retryIntervalMilliseconds) { + return new VaultImpl(vaultConfig, new RetryPolicy(maxRetries, retryIntervalMilliseconds)); + } + /** * Returns the implementing class for Vault's core/logical operations (e.g. read, write). * @@ -172,7 +209,7 @@ public VaultImpl withRetries(final int maxRetries, final int retryIntervalMillis */ @Override public Logical logical() { - return new Logical(vaultConfig); + return new Logical(vaultConfig, retryPolicy); } /** @@ -183,7 +220,7 @@ public Logical logical() { */ @Override public Auth auth() { - return new Auth(vaultConfig); + return new Auth(vaultConfig, retryPolicy); } /** @@ -194,7 +231,7 @@ public Auth auth() { */ @Override public Sys sys() { - return new Sys(vaultConfig); + return new Sys(vaultConfig, retryPolicy); } /** @@ -205,7 +242,7 @@ public Sys sys() { */ @Override public Pki pki() { - return new Pki(vaultConfig); + return new Pki(vaultConfig, "pki", retryPolicy); } /** @@ -232,17 +269,17 @@ public Pki pki() { */ @Override public Pki pki(final String mountPath) { - return new Pki(vaultConfig, mountPath); + return new Pki(vaultConfig, mountPath, retryPolicy); } @Override public Database database() { - return new Database(vaultConfig); + return new Database(vaultConfig, "database", retryPolicy); } @Override public Database database(final String mountPath) { - return new Database(vaultConfig, mountPath); + return new Database(vaultConfig, mountPath, retryPolicy); } /** @@ -255,7 +292,7 @@ public Database database(final String mountPath) { @Override @Deprecated public Leases leases() { - return new Leases(vaultConfig); + return new Leases(vaultConfig, retryPolicy); } /** @@ -265,7 +302,7 @@ public Leases leases() { */ @Override public Debug debug() { - return new Debug(vaultConfig); + return new Debug(vaultConfig, retryPolicy); } /** @@ -279,7 +316,7 @@ public Debug debug() { @Override @Deprecated public Mounts mounts() { - return new Mounts(vaultConfig); + return new Mounts(vaultConfig, retryPolicy); } /** @@ -292,7 +329,7 @@ public Mounts mounts() { @Override @Deprecated public Seal seal() { - return new Seal(vaultConfig); + return new Seal(vaultConfig, retryPolicy); } /** diff --git a/src/test/java/io/github/jopenlibs/vault/RetryTests.java b/src/test/java/io/github/jopenlibs/vault/RetryTests.java index 2b021f9b..e98e5941 100644 --- a/src/test/java/io/github/jopenlibs/vault/RetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/RetryTests.java @@ -1,11 +1,18 @@ package io.github.jopenlibs.vault; +import io.github.jopenlibs.vault.api.OperationsBase; import io.github.jopenlibs.vault.mock.RetriesMockVault; import io.github.jopenlibs.vault.response.LogicalResponse; +import java.util.List; import java.util.Map; import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; /** *

Unit tests for the Vault driver, having no dependency on an actual Vault server instance @@ -117,4 +124,136 @@ public void testNoRetries_Write_400() throws Exception { VaultTestUtils.shutdownMockVault(server); } + private static final String READ_RESPONSE = + "{\"lease_id\":\"12345\",\"renewable\":false,\"lease_duration\":10000,\"data\":{\"value\":\"mock\"}}"; + + @Test + public void testRetrying_Read() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(5, 200, READ_RESPONSE)); + server.start(); + try { + final var vault = Vault.create(mockConfig()); + final LogicalResponse response = vault.retrying(5, 10).logical().read("secret/hello"); + assertEquals(5, response.getRetries()); + assertEquals("mock", response.getData().get("value")); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_LeavesOriginalUnchanged() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(1, 200, READ_RESPONSE)); + server.start(); + try { + final var config = mockConfig(); + final var vault = Vault.create(config); + assertNotSame(vault, vault.retrying(5, 10)); + assertEquals(0, config.getMaxRetries()); + assertEquals(0, config.getRetryIntervalMilliseconds()); + + final var e = assertThrows(VaultException.class, + () -> vault.logical().read("secret/hello")); + assertEquals(500, e.getHttpStatusCode()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_OverridesConfigRetries() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(3, 200, READ_RESPONSE)); + server.start(); + try { + final var vault = Vault.create(mockConfig()); + vault.withRetries(5, 10); + // 1 retry is fewer than the 3 failures, while the config retries would be enough + final var e = assertThrows(VaultException.class, + () -> vault.retrying(1, 10).logical().read("secret/hello")); + assertEquals(500, e.getHttpStatusCode()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_SysLeasesGetsPolicy() throws Exception { + final var server = VaultTestUtils.initHttpMockVault(new RetriesMockVault(2, 204, null)); + server.start(); + try { + final var vault = Vault.create(mockConfig()); + final var response = vault.retrying(2, 10).sys().leases().revoke("lease-id"); + assertEquals(2, response.getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + + @Test + public void testRetrying_SharesConfig() throws Exception { + final var config = mockConfig(); + final var retrying = Vault.create(config).retrying(5, 10); + assertSame(config, baseFieldOf(retrying.auth(), "config")); + config.token("new_token"); + assertArrayEquals("new_token".toCharArray(), + ((VaultConfig) baseFieldOf(retrying.logical(), "config")).getToken()); + } + + @Test + @SuppressWarnings("deprecation") + public void testRetrying_EveryOperationGetsPolicy() throws Exception { + final var vault = Vault.create(mockConfig()); + final var policy = new RetryPolicy(5, 10); + final var retrying = vault.retrying(5, 10); + for (final OperationsBase operations : List.of(retrying.logical(), retrying.auth(), + retrying.sys(), retrying.pki(), retrying.pki("root-ca"), retrying.database(), + retrying.database("db123"), retrying.leases(), retrying.debug(), + retrying.mounts(), retrying.seal())) { + assertEquals(operations.getClass().getName(), policy, retryPolicyOf(operations)); + } + for (final OperationsBase operations : List.of(vault.logical(), vault.pki(), + vault.database())) { + assertNull(operations.getClass().getName(), retryPolicyOf(operations)); + } + } + + @Test + public void testRetrying_PkiAndDatabaseUseDefaultMountPaths() throws Exception { + final var retrying = Vault.create(mockConfig()).retrying(5, 10); + assertEquals("pki", fieldOf(retrying.pki(), "mountPath")); + assertEquals("database", fieldOf(retrying.database(), "mountPath")); + } + + @Test + public void testRetrying_RejectsNegativeValues() throws Exception { + final var vault = Vault.create(mockConfig()); + assertThrows(IllegalArgumentException.class, () -> vault.retrying(-1, 10)); + assertThrows(IllegalArgumentException.class, () -> vault.retrying(5, -1)); + } + + private static VaultConfig mockConfig() throws VaultException { + return new VaultConfig().address("http://127.0.0.1:8999") + .token("mock_token").engineVersion(1).build(); + } + + private static Object retryPolicyOf(final OperationsBase operations) throws Exception { + return baseFieldOf(operations, "retryPolicy"); + } + + private static Object baseFieldOf(final OperationsBase operations, final String name) + throws Exception { + final var field = OperationsBase.class.getDeclaredField(name); + field.setAccessible(true); + return field.get(operations); + } + + private static Object fieldOf(final Object target, final String name) throws Exception { + final var field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + } From 63b02ad381291665c18a77575800a7aa3e0effa5 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 15 Oct 2026 00:06:02 +0200 Subject: [PATCH 05/18] Added builder method for retries --- .../github/jopenlibs/vault/RetryPolicy.java | 1 + .../java/io/github/jopenlibs/vault/Vault.java | 1 + .../github/jopenlibs/vault/VaultConfig.java | 26 +++++++++ .../io/github/jopenlibs/vault/RetryTests.java | 56 +++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java index 06d9b09c..9682bc61 100644 --- a/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java +++ b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java @@ -7,6 +7,7 @@ *

A policy with maxRetries set to 0 disables retries.

* * @see Vault#retrying(int, int) + * @see VaultConfig#retries(int, int) */ public final class RetryPolicy { diff --git a/src/main/java/io/github/jopenlibs/vault/Vault.java b/src/main/java/io/github/jopenlibs/vault/Vault.java index 89e15a7d..a4dcfae9 100644 --- a/src/main/java/io/github/jopenlibs/vault/Vault.java +++ b/src/main/java/io/github/jopenlibs/vault/Vault.java @@ -86,6 +86,7 @@ static Vault create(final VaultConfig vaultConfig, final Boolean useSecretsEngin * @throws IllegalArgumentException If either value is negative * @throws UnsupportedOperationException If this implementation does not support it * @see RetryPolicy + * @see VaultConfig#retries(int, int) */ default Vault retrying(final int maxRetries, final int retryIntervalMilliseconds) { throw new UnsupportedOperationException( diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index c480c487..add1bc5c 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -319,6 +319,32 @@ public VaultConfig httpClient(HttpClient httpClient) { return this; } + /** + *

Sets the retry rules for every API operation using this configuration: the maximum number + * of times that a failing operation will be retried, and the time to wait between attempts. + * By default, operations are not retried.

+ * + *

To retry only some calls, use Vault.retrying() instead, which overrides + * these values.

+ * + * @param maxRetries The number of times that API operations will be retried when a failure + * occurs + * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in + * between retries + * @return This object, with maxRetries and retryIntervalMilliseconds populated, ready for + * additional builder-pattern method calls or else finalization with the build() method + * @throws IllegalArgumentException If either value is negative, in which case the previous + * values are kept + * @see Vault#retrying(int, int) + * @see RetryPolicy + */ + public VaultConfig retries(final int maxRetries, final int retryIntervalMilliseconds) { + final var policy = new RetryPolicy(maxRetries, retryIntervalMilliseconds); + this.maxRetries = policy.getMaxRetries(); + this.retryIntervalMilliseconds = policy.getRetryIntervalMilliseconds(); + return this; + } + /** *

Sets the maximum number of times that an API operation will retry upon failure.

* diff --git a/src/test/java/io/github/jopenlibs/vault/RetryTests.java b/src/test/java/io/github/jopenlibs/vault/RetryTests.java index e98e5941..6b657998 100644 --- a/src/test/java/io/github/jopenlibs/vault/RetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/RetryTests.java @@ -234,6 +234,62 @@ public void testRetrying_RejectsNegativeValues() throws Exception { assertThrows(IllegalArgumentException.class, () -> vault.retrying(5, -1)); } + @Test + public void testConfigRetries_SetsValues() throws Exception { + final var config = new VaultConfig(); + assertSame(config, config.retries(5, 1000)); + assertEquals(5, config.getMaxRetries()); + assertEquals(1000, config.getRetryIntervalMilliseconds()); + } + + @Test + public void testConfigRetries_AppliesToEveryCall() throws Exception { + final var vault = Vault.create(new VaultConfig().address("http://127.0.0.1:8999") + .token("mock_token").engineVersion(1).retries(2, 10).build()); + + final var readServer = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(2, 200, READ_RESPONSE)); + readServer.start(); + try { + assertEquals(2, vault.logical().read("secret/hello").getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(readServer); + } + + final var revokeServer = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(2, 204, null)); + revokeServer.start(); + try { + assertEquals(2, vault.sys().leases().revoke("lease-id").getRetries()); + } finally { + VaultTestUtils.shutdownMockVault(revokeServer); + } + } + + @Test + public void testConfigRetries_RejectsNegativeValues() throws Exception { + final var config = new VaultConfig().retries(5, 1000); + assertThrows(IllegalArgumentException.class, () -> config.retries(-1, 10)); + assertThrows(IllegalArgumentException.class, () -> config.retries(3, -1)); + assertEquals(5, config.getMaxRetries()); + assertEquals(1000, config.getRetryIntervalMilliseconds()); + } + + @Test + public void testConfigRetries_OverriddenByRetrying() throws Exception { + final var server = VaultTestUtils.initHttpMockVault( + new RetriesMockVault(3, 200, READ_RESPONSE)); + server.start(); + try { + final var vault = Vault.create(mockConfig().retries(5, 10)); + final var e = assertThrows(VaultException.class, + () -> vault.retrying(1, 10).logical().read("secret/hello")); + assertEquals(500, e.getHttpStatusCode()); + } finally { + VaultTestUtils.shutdownMockVault(server); + } + } + private static VaultConfig mockConfig() throws VaultException { return new VaultConfig().address("http://127.0.0.1:8999") .token("mock_token").engineVersion(1).build(); From 3771e871e09b277d8c2bd712d4e4bf81deb65b03 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 15 Oct 2026 00:10:08 +0200 Subject: [PATCH 06/18] Deprecated withRetries() method --- .../java/io/github/jopenlibs/vault/Vault.java | 15 ++++++++++--- .../github/jopenlibs/vault/VaultConfig.java | 8 +++---- .../io/github/jopenlibs/vault/VaultImpl.java | 17 +++++++++++---- .../io/github/jopenlibs/vault/RetryTests.java | 21 +++++++++++++++++++ 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/Vault.java b/src/main/java/io/github/jopenlibs/vault/Vault.java index a4dcfae9..e696d542 100644 --- a/src/main/java/io/github/jopenlibs/vault/Vault.java +++ b/src/main/java/io/github/jopenlibs/vault/Vault.java @@ -50,15 +50,24 @@ static Vault create(final VaultConfig vaultConfig, final Boolean useSecretsEngin } /** - * This method is chained ahead of endpoints (e.g. logical(), auth(), - * etc... to specify retry rules for any API operations invoked on that endpoint. + *

Sets the retry rules for API operations, by writing them to the configuration of this + * instance.

+ * + *

Despite being chained ahead of endpoints (e.g. logical(), + * auth()), the retries are not limited to that call: they apply to every later + * call on this instance, and on every other Vault sharing the same configuration. + * A Vault returned by {@link #retrying(int, int)} ignores them.

* * @param maxRetries The number of times that API operations will be retried when a failure * occurs * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in * between retries - * @return This object, with maxRetries and retryIntervalMilliseconds populated + * @return This object, with maxRetries and retryIntervalMilliseconds populated in its + * configuration + * @deprecated To retry only some calls, use {@link #retrying(int, int)}, which returns a new + * instance. To retry every call, use {@link VaultConfig#retries(int, int)}. */ + @Deprecated Vault withRetries(final int maxRetries, final int retryIntervalMilliseconds); /** diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index add1bc5c..190337d3 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -349,8 +349,8 @@ public VaultConfig retries(final int maxRetries, final int retryIntervalMillisec *

Sets the maximum number of times that an API operation will retry upon failure.

* *

This method is not meant to be called from application-level code outside of this package - * (hence the protected access level. It is meant to be invoked via - * Vault.withRetries() in a builder pattern DSL-style.

+ * (hence the package-private access level). It is used by the deprecated + * Vault.withRetries(): use {@link #retries(int, int)} instead.

* * @param maxRetries The number of times that API operations will be retried when a failure * occurs. @@ -364,8 +364,8 @@ void setMaxRetries(final int maxRetries) { * attempts for a failing API operation.

* *

This method is not meant to be called from application-level code outside of this package - * (hence the protected access level. It is meant to be invoked via - * Vault.withRetries() in a builder pattern DSL-style.

+ * (hence the package-private access level). It is used by the deprecated + * Vault.withRetries(): use {@link #retries(int, int)} instead.

* * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in * between retries. diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index daa15a7d..64bce224 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -43,7 +43,7 @@ * secrets.put("other_value", "You can store multiple name/value pairs under a given key"); * * final LogicalResponse writeResponse = vault - * .withRetries(5, 1000) // optional + * .retrying(5, 1000) // optional * .logical() * .write("secret/hello", secrets); * @@ -166,16 +166,25 @@ private void logNameSpace() { } /** - * This method is chained ahead of endpoints (e.g. logical(), auth(), - * etc... to specify retry rules for any API operations invoked on that endpoint. + *

Sets the retry rules for API operations, by writing them to the configuration of this + * instance.

+ * + *

Despite being chained ahead of endpoints (e.g. logical(), + * auth()), the retries are not limited to that call: they apply to every later + * call on this instance, and on every other Vault sharing the same configuration. + * A Vault returned by {@link #retrying(int, int)} ignores them.

* * @param maxRetries The number of times that API operations will be retried when a failure * occurs * @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in * between retries - * @return This object, with maxRetries and retryIntervalMilliseconds populated + * @return This object, with maxRetries and retryIntervalMilliseconds populated in its + * configuration + * @deprecated To retry only some calls, use {@link #retrying(int, int)}, which returns a new + * instance. To retry every call, use {@link VaultConfig#retries(int, int)}. */ @Override + @Deprecated public VaultImpl withRetries(final int maxRetries, final int retryIntervalMilliseconds) { this.vaultConfig.setMaxRetries(maxRetries); this.vaultConfig.setRetryIntervalMilliseconds(retryIntervalMilliseconds); diff --git a/src/test/java/io/github/jopenlibs/vault/RetryTests.java b/src/test/java/io/github/jopenlibs/vault/RetryTests.java index 6b657998..cb47620d 100644 --- a/src/test/java/io/github/jopenlibs/vault/RetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/RetryTests.java @@ -290,6 +290,27 @@ public void testConfigRetries_OverriddenByRetrying() throws Exception { } } + @Test + @SuppressWarnings("deprecation") + public void testWithRetries_Unchanged() throws Exception { + final var config = mockConfig(); + final var vault = Vault.create(config); + final var other = Vault.create(config); + final var retrying = vault.retrying(1, 10); + + assertSame(vault, vault.withRetries(5, 1000)); + assertEquals(5, config.getMaxRetries()); + assertEquals(1000, config.getRetryIntervalMilliseconds()); + // Shared by every Vault using the config, but ignored by one returned by retrying() + assertNull(retryPolicyOf(other.logical())); + assertEquals(new RetryPolicy(1, 10), retryPolicyOf(retrying.logical())); + + // Negative values are still written to the config, not validated + vault.withRetries(-1, -1); + assertEquals(-1, config.getMaxRetries()); + assertEquals(-1, config.getRetryIntervalMilliseconds()); + } + private static VaultConfig mockConfig() throws VaultException { return new VaultConfig().address("http://127.0.0.1:8999") .token("mock_token").engineVersion(1).build(); From 146faefe4d30fc9fecade076b7f490863b343500 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 15 Oct 2026 00:11:57 +0200 Subject: [PATCH 07/18] Updated README --- README.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 47971243..c0ff308e 100644 --- a/README.md +++ b/README.md @@ -326,16 +326,31 @@ Vault's HTTP API: credentials). * `debug()`: Health check endpoints. -The driver DSL also allows you to specify retry logic, by chaining the `withRetries()` ahead of -accessing the endpoint implementation: +By default, API operations are not retried. To retry every call, set the retries in the +configuration: ``` // Retry up to 5 times if failures occur, waiting 1000 milliseconds in between each retry attempt. -final LogicalResponse response = vault.withRetries(5, 1000) +final VaultConfig config = new VaultConfig() + .address("http://127.0.0.1:8200") + .token("3c9fd6be-7bc2-9d1f-6fb3-cd746c0fc4e8") + .retries(5, 1000) + .build(); +``` + +To retry only some calls, chain `retrying()` ahead of accessing the endpoint implementation. It +returns a new `Vault` sharing the same configuration, and leaves the original `vault` unchanged: + +``` +final LogicalResponse response = vault.retrying(5, 1000) .logical() .read("secret/hello"); ``` +`withRetries()` is deprecated: despite being chained like `retrying()`, it changes the retries in the +configuration, so they apply to every later call, and to every `Vault` sharing that configuration. +Replace it with `retrying()` or `VaultConfig.retries()`. + Only transient failures are retried: I/O errors (e.g. connection refused or timed out) and HTTP 5xx, 408, 412 and 429 responses. Other 4xx responses (e.g. 400, 403, 404) fail immediately, since retrying them would not help. @@ -407,6 +422,17 @@ patches. * Javadoc examples use the `Sys` methods instead of the deprecated ones. The `Wrapping` examples are fixed: they used `new Vault(config)` instead of `Vault.create(config)`, and a misspelled `lookupWarp()` method + * Retries can be limited to some calls [(issue #75)](https://github.com/jopenlibs/vault-java-driver/issues/75). + `Vault.retrying(maxRetries, retryIntervalMilliseconds)` returns a new `Vault` whose operations + use those retries, leaving the original instance and its configuration unchanged. + `VaultConfig.retries(maxRetries, retryIntervalMilliseconds)` sets retries for every call. + Both reject negative values with `IllegalArgumentException`. The operation classes (e.g. + `Logical`, `Sys`, `Pki`) have new constructors taking a `RetryPolicy` + * `Vault.withRetries()` is deprecated, with unchanged behaviour: it writes the retries to the + shared `VaultConfig`, so they apply to every later call and to every `Vault` sharing that + configuration, not only the chained call. Use `retrying()` or `VaultConfig.retries()` instead. + `Vault.retrying()` is a `default` method throwing `UnsupportedOperationException`, so custom + implementations of `Vault` keep compiling * **6.2.3**: This release contains the following updates: * Improved Java 11 compatibility * Fixed integration tests with Vault 2.x From 95ef4a54d755696809be53cfc9e186138e53bc46 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 01:01:01 +0200 Subject: [PATCH 08/18] Updated test to adapt to latest version of Vault --- .../java/io/github/jopenlibs/vault/api/MountsIT.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java b/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java index 6c5e2388..ee96719a 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java +++ b/src/test/java/io/github/jopenlibs/vault/api/MountsIT.java @@ -20,6 +20,7 @@ import org.junit.rules.ExpectedException; import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; /** @@ -152,12 +153,13 @@ public void testRead() throws VaultException { @Test public void testReadExceptionNotFound() throws VaultException { - expectedEx.expect(VaultException.class); - expectedEx.expectMessage("cannot fetch sysview for path"); - final Vault vault = container.getRootVault(); - vault.sys().mounts().read("pki-non-existing-path"); + // Vault 2.2 and later answer "invalid request" instead of "cannot fetch sysview for path", + // so only the status code is checked + final VaultException e = assertThrows(VaultException.class, + () -> vault.sys().mounts().read("pki-non-existing-path")); + assertEquals(400, e.getHttpStatusCode()); } @Test From 34bb410b412626f1ad07d7d4435fa60398614b5a Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 01:01:48 +0200 Subject: [PATCH 09/18] Added new version in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39ec971f..71fdd2a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: - 2.1.0 - 2.1.1 - 2.1.2 + - 2.2.0 - latest os: - ubuntu-latest From 9c314d57f5709e974968a7ce1c9f3533e9c900f7 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 00:38:47 +0200 Subject: [PATCH 10/18] Updated vendored JSON library --- .../github/jopenlibs/vault/api/Logical.java | 6 +- .../vault/api/sys/mounts/MountPayload.java | 48 +- .../io/github/jopenlibs/vault/json/Json.java | 537 ++--- .../jopenlibs/vault/json/JsonArray.java | 800 ++++---- .../jopenlibs/vault/json/JsonHandler.java | 263 +++ .../jopenlibs/vault/json/JsonLiteral.java | 112 +- .../jopenlibs/vault/json/JsonNumber.java | 104 +- .../jopenlibs/vault/json/JsonObject.java | 1692 ++++++++-------- .../jopenlibs/vault/json/JsonParser.java | 774 ++++--- .../jopenlibs/vault/json/JsonString.java | 72 +- .../jopenlibs/vault/json/JsonValue.java | 674 ++++--- .../jopenlibs/vault/json/JsonWriter.java | 212 +- .../github/jopenlibs/vault/json/Location.java | 76 + .../jopenlibs/vault/json/ParseException.java | 85 +- .../jopenlibs/vault/json/PrettyPrint.java | 204 +- .../jopenlibs/vault/json/WriterConfig.java | 31 +- .../jopenlibs/vault/json/WritingBuffer.java | 105 +- .../vault/response/MountResponse.java | 34 +- .../jopenlibs/vault/json/JsonArray_Test.java | 1048 +++++----- .../vault/json/JsonLiteral_Test.java | 273 ++- .../jopenlibs/vault/json/JsonNumber_Test.java | 289 +-- .../jopenlibs/vault/json/JsonObject_Test.java | 1771 +++++++++-------- .../jopenlibs/vault/json/JsonParser_Test.java | 1158 +++++++---- .../jopenlibs/vault/json/JsonString_Test.java | 177 +- .../jopenlibs/vault/json/JsonValue_Test.java | 332 +-- .../jopenlibs/vault/json/JsonWriter_Test.java | 221 +- .../jopenlibs/vault/json/Json_Test.java | 446 +++-- .../vault/json/ParseException_Test.java | 44 +- .../vault/json/PrettyPrint_Test.java | 197 +- .../github/jopenlibs/vault/json/TestUtil.java | 133 +- .../vault/json/WritingBuffer_Test.java | 241 ++- .../vault/json/test/mocking/Mocking_Test.java | 39 +- 32 files changed, 6822 insertions(+), 5376 deletions(-) create mode 100644 src/main/java/io/github/jopenlibs/vault/json/JsonHandler.java create mode 100644 src/main/java/io/github/jopenlibs/vault/json/Location.java diff --git a/src/main/java/io/github/jopenlibs/vault/api/Logical.java b/src/main/java/io/github/jopenlibs/vault/api/Logical.java index 9d3fbbcb..9844a357 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -442,7 +442,7 @@ public LogicalResponse delete(final String path, final int[] versions) throws Va return retry(attempt -> { // Make an HTTP request to Vault - final var versionsToDelete = new JsonObject().add("versions", versions); + final var versionsToDelete = new JsonObject().add("versions", Json.array(versions)); final var restResponse = getRest()//NOPMD .url(config.getAddress() + "/v1/" + adjustPathForVersionDelete(path, config.getPrefixPathDepth())) @@ -499,7 +499,7 @@ public LogicalResponse unDelete(final String path, final int[] versions) throws return retry(attempt -> { // Make an HTTP request to Vault - final var versionsToUnDelete = new JsonObject().add("versions", versions); + final var versionsToUnDelete = new JsonObject().add("versions", Json.array(versions)); final var restResponse = getRest() //NOPMD .url(config.getAddress() + "/v1/" + adjustPathForVersionUnDelete(path, config.getPrefixPathDepth())) @@ -543,7 +543,7 @@ public LogicalResponse destroy(final String path, final int[] versions) throws V return retry(attempt -> { // Make an HTTP request to Vault - final var versionsToDestroy = new JsonObject().add("versions", versions); + final var versionsToDestroy = new JsonObject().add("versions", Json.array(versions)); final var restResponse = getRest()//NOPMD .url(config.getAddress() + "/v1/" + adjustPathForVersionDestroy(path, config.getPrefixPathDepth())) diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/MountPayload.java b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/MountPayload.java index 8888a9a9..9292c7b8 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/MountPayload.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/MountPayload.java @@ -2,6 +2,7 @@ import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.json.JsonValue; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -101,12 +102,12 @@ public List getAuditNonHmacResponseKeys() { public JsonObject toEnableJson(MountType type) { final JsonObject jsonObject = Json.object(); - jsonObject.addIfNotNull("type", type.value()); - jsonObject.addIfNotNull("description", this.description); - jsonObject.addIfNotNull("config", this.toConfigJson()); - jsonObject.addIfNotNull("plugin_name", this.pluginName); - jsonObject.addIfNotNull("local", this.local); - jsonObject.addIfNotNull("seal_wrap", this.sealWrap); + addIfNotNull(jsonObject, "type", type.value()); + addIfNotNull(jsonObject, "description", this.description); + addIfNotNull(jsonObject, "config", this.toConfigJson()); + addIfNotNull(jsonObject, "plugin_name", this.pluginName); + addIfNotNull(jsonObject, "local", this.local); + addIfNotNull(jsonObject, "seal_wrap", this.sealWrap); return jsonObject; } @@ -115,22 +116,22 @@ public JsonObject toTuneJson() { final JsonObject jsonObject = Json.object(); if (this.defaultLeaseTtl != null) { - jsonObject.addIfNotNull("default_lease_ttl", this.defaultLeaseTtl.toString()); + addIfNotNull(jsonObject, "default_lease_ttl", this.defaultLeaseTtl.toString()); } if (this.maxLeaseTtl != null) { - jsonObject.addIfNotNull("max_lease_ttl", this.maxLeaseTtl.toString()); + addIfNotNull(jsonObject, "max_lease_ttl", this.maxLeaseTtl.toString()); } - jsonObject.addIfNotNull("description", this.description); + addIfNotNull(jsonObject, "description", this.description); if (this.auditNonHmacRequestKeys != null && !this.auditNonHmacRequestKeys.isEmpty()) { - jsonObject.addIfNotNull("audit_non_hmac_request_keys", + addIfNotNull(jsonObject, "audit_non_hmac_request_keys", String.join(",", this.auditNonHmacRequestKeys)); } if (this.auditNonHmacResponseKeys != null && !this.auditNonHmacResponseKeys.isEmpty()) { - jsonObject.addIfNotNull("audit_non_hmac_response_keys", + addIfNotNull(jsonObject, "audit_non_hmac_response_keys", String.join(",", this.auditNonHmacResponseKeys)); } @@ -140,8 +141,8 @@ public JsonObject toTuneJson() { private JsonObject toConfigJson() { final JsonObject jsonObject = toTuneJson(); - jsonObject.addIfNotNull("force_no_cache", this.forceNoCache); - jsonObject.addIfNotNull("plugin_name", this.pluginName); + addIfNotNull(jsonObject, "force_no_cache", this.forceNoCache); + addIfNotNull(jsonObject, "plugin_name", this.pluginName); if (jsonObject.isEmpty()) { return null; @@ -150,6 +151,27 @@ private JsonObject toConfigJson() { return jsonObject; } + private static void addIfNotNull(final JsonObject jsonObject, final String name, + final String value) { + if (value != null) { + jsonObject.add(name, value); + } + } + + private static void addIfNotNull(final JsonObject jsonObject, final String name, + final Boolean value) { + if (value != null) { + jsonObject.add(name, value.booleanValue()); + } + } + + private static void addIfNotNull(final JsonObject jsonObject, final String name, + final JsonValue value) { + if (value != null) { + jsonObject.add(name, value); + } + } + public TimeToLive getDefaultLeaseTtl() { return defaultLeaseTtl; } diff --git a/src/main/java/io/github/jopenlibs/vault/json/Json.java b/src/main/java/io/github/jopenlibs/vault/json/Json.java index f9a139f9..49b53fe8 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/Json.java +++ b/src/main/java/io/github/jopenlibs/vault/json/Json.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 EclipseSource. + * Copyright (c) 2015, 2016 EclipseSource. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,6 @@ import java.io.IOException; import java.io.Reader; -import java.util.Arrays; /** @@ -37,8 +36,8 @@ * *

* To create a JSON data structure to be serialized, use the methods - * value(), array(), and object(). For example, the - * following snippet will produce the JSON string {"foo": 23, "bar": true}: + * value(), array(), and object(). For example, the following + * snippet will produce the JSON string {"foo": 23, "bar": true}: *

*
  * String string = Json.object().add("foo", 23).add("bar", true).toString();
@@ -52,277 +51,341 @@
  * JsonArray array = Json.array(names);
  * 
*/ -@SuppressWarnings("PMD") public final class Json { - private Json() { - // not meant to be instantiated + private Json() { + // not meant to be instantiated + } + + /** + * Represents the JSON literal null. + */ + public static final JsonValue NULL = new JsonLiteral("null"); + + /** + * Represents the JSON literal true. + */ + public static final JsonValue TRUE = new JsonLiteral("true"); + + /** + * Represents the JSON literal false. + */ + public static final JsonValue FALSE = new JsonLiteral("false"); + + /** + * Returns a JsonValue instance that represents the given int value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue value(int value) { + return new JsonNumber(Integer.toString(value, 10)); + } + + /** + * Returns a JsonValue instance that represents the given long value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue value(long value) { + return new JsonNumber(Long.toString(value, 10)); + } + + /** + * Returns a JsonValue instance that represents the given float value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue value(float value) { + if (Float.isInfinite(value) || Float.isNaN(value)) { + throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON"); } + return new JsonNumber(cutOffPointZero(Float.toString(value))); + } - /** - * Represents the JSON literal null. - */ - public static final JsonValue NULL = new JsonLiteral("null"); - - /** - * Represents the JSON literal true. - */ - public static final JsonValue TRUE = new JsonLiteral("true"); - - /** - * Represents the JSON literal false. - */ - public static final JsonValue FALSE = new JsonLiteral("false"); - - /** - * Returns a JsonValue instance that represents the given int value. - * - * @param value the value to get a JSON representation for - * @return a JSON value that represents the given value - */ - public static JsonValue value(int value) { - return new JsonNumber(Integer.toString(value, 10)); + /** + * Returns a JsonValue instance that represents the given double value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue value(double value) { + if (Double.isInfinite(value) || Double.isNaN(value)) { + throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON"); } + return new JsonNumber(cutOffPointZero(Double.toString(value))); + } + + /** + * Returns a JsonValue instance that represents the given string. + * + * @param string + * the string to get a JSON representation for + * @return a JSON value that represents the given string + */ + public static JsonValue value(String string) { + return string == null ? NULL : new JsonString(string); + } - /** - * Returns a JsonValue instance that represents the given int[] value. - * - * @param value the value to get a JSON representation for - * @return a JSON value that represents the given value - */ - public static JsonValue value(int[] value) { - return new JsonNumber(Arrays.toString(value)); + /** + * Returns a JsonValue instance that represents the given boolean value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + */ + public static JsonValue value(boolean value) { + return value ? TRUE : FALSE; + } + + /** + * Creates a new empty JsonArray. This is equivalent to creating a new JsonArray using the + * constructor. + * + * @return a new empty JSON array + */ + public static JsonArray array() { + return new JsonArray(); + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given int + * values. + * + * @param values + * the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(int... values) { + if (values == null) { + throw new NullPointerException("values is null"); + } + JsonArray array = new JsonArray(); + for (int value : values) { + array.add(value); } + return array; + } - /** - * Returns a JsonValue instance that represents the given long value. - * - * @param value the value to get a JSON representation for - * @return a JSON value that represents the given value - */ - public static JsonValue value(long value) { - return new JsonNumber(Long.toString(value, 10)); + /** + * Creates a new JsonArray that contains the JSON representations of the given long + * values. + * + * @param values + * the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(long... values) { + if (values == null) { + throw new NullPointerException("values is null"); } + JsonArray array = new JsonArray(); + for (long value : values) { + array.add(value); + } + return array; + } - /** - * Returns a JsonValue instance that represents the given float value. - * - * @param value the value to get a JSON representation for - * @return a JSON value that represents the given value - */ - public static JsonValue value(float value) { - if (Float.isInfinite(value) || Float.isNaN(value)) { - throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON"); - } - return new JsonNumber(cutOffPointZero(Float.toString(value))); + /** + * Creates a new JsonArray that contains the JSON representations of the given float + * values. + * + * @param values + * the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(float... values) { + if (values == null) { + throw new NullPointerException("values is null"); + } + JsonArray array = new JsonArray(); + for (float value : values) { + array.add(value); } + return array; + } - /** - * Returns a JsonValue instance that represents the given double value. - * - * @param value the value to get a JSON representation for - * @return a JSON value that represents the given value - */ - public static JsonValue value(double value) { - if (Double.isInfinite(value) || Double.isNaN(value)) { - throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON"); - } - return new JsonNumber(cutOffPointZero(Double.toString(value))); + /** + * Creates a new JsonArray that contains the JSON representations of the given double + * values. + * + * @param values + * the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(double... values) { + if (values == null) { + throw new NullPointerException("values is null"); + } + JsonArray array = new JsonArray(); + for (double value : values) { + array.add(value); } + return array; + } - /** - * Returns a JsonValue instance that represents the given string. - * - * @param string the string to get a JSON representation for - * @return a JSON value that represents the given string - */ - public static JsonValue value(String string) { - return string == null ? NULL : new JsonString(string); + /** + * Creates a new JsonArray that contains the JSON representations of the given + * boolean values. + * + * @param values + * the values to be included in the new JSON array + * @return a new JSON array that contains the given values + */ + public static JsonArray array(boolean... values) { + if (values == null) { + throw new NullPointerException("values is null"); } + JsonArray array = new JsonArray(); + for (boolean value : values) { + array.add(value); + } + return array; + } + + /** + * Creates a new JsonArray that contains the JSON representations of the given strings. + * + * @param strings + * the strings to be included in the new JSON array + * @return a new JSON array that contains the given strings + */ + public static JsonArray array(String... strings) { + if (strings == null) { + throw new NullPointerException("values is null"); + } + JsonArray array = new JsonArray(); + for (String value : strings) { + array.add(value); + } + return array; + } + + /** + * Creates a new empty JsonObject. This is equivalent to creating a new JsonObject using the + * constructor. + * + * @return a new empty JSON object + */ + public static JsonObject object() { + return new JsonObject(); + } + + /** + * Parses the given input string as JSON. The input must contain a valid JSON value, optionally + * padded with whitespace. + * + * @param string + * the input string, must be valid JSON + * @return a value that represents the parsed JSON + * @throws ParseException + * if the input is not valid JSON + */ + public static JsonValue parse(String string) { + if (string == null) { + throw new NullPointerException("string is null"); + } + DefaultHandler handler = new DefaultHandler(); + new JsonParser(handler).parse(string); + return handler.getValue(); + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input must contain a + * valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into an input buffer. Hence, wrapping a reader in an additional + * BufferedReader likely won't improve reading performance. + *

+ * + * @param reader + * the reader to read the JSON value from + * @return a value that represents the parsed JSON + * @throws IOException + * if an I/O error occurs in the reader + * @throws ParseException + * if the input is not valid JSON + */ + public static JsonValue parse(Reader reader) throws IOException { + if (reader == null) { + throw new NullPointerException("reader is null"); + } + DefaultHandler handler = new DefaultHandler(); + new JsonParser(handler).parse(reader); + return handler.getValue(); + } - /** - * Returns a JsonValue instance that represents the given boolean value. - * - * @param value the value to get a JSON representation for - * @return a JSON value that represents the given value - */ - public static JsonValue value(boolean value) { - return value ? TRUE : FALSE; + private static String cutOffPointZero(String string) { + if (string.endsWith(".0")) { + return string.substring(0, string.length() - 2); } + return string; + } - /** - * Creates a new empty JsonArray. This is equivalent to creating a new JsonArray using the - * constructor. - * - * @return a new empty JSON array - */ - public static JsonValue array() { - return new JsonArray(); + static class DefaultHandler extends JsonHandler { + + protected JsonValue value; + + @Override + public JsonArray startArray() { + return new JsonArray(); } - /** - * Creates a new JsonArray that contains the JSON representations of the given int - * values. - * - * @param values the values to be included in the new JSON array - * @return a new JSON array that contains the given values - */ - public static JsonArray array(int... values) { - if (values == null) { - throw new NullPointerException("values is null"); - } - JsonArray array = new JsonArray(); - for (int value : values) { - array.add(value); - } - return array; + @Override + public JsonObject startObject() { + return new JsonObject(); } - /** - * Creates a new JsonArray that contains the JSON representations of the given long - * values. - * - * @param values the values to be included in the new JSON array - * @return a new JSON array that contains the given values - */ - public static JsonArray array(long... values) { - if (values == null) { - throw new NullPointerException("values is null"); - } - JsonArray array = new JsonArray(); - for (long value : values) { - array.add(value); - } - return array; + @Override + public void endNull() { + value = NULL; } - /** - * Creates a new JsonArray that contains the JSON representations of the given - * float values. - * - * @param values the values to be included in the new JSON array - * @return a new JSON array that contains the given values - */ - public static JsonArray array(float... values) { - if (values == null) { - throw new NullPointerException("values is null"); - } - JsonArray array = new JsonArray(); - for (float value : values) { - array.add(value); - } - return array; + @Override + public void endBoolean(boolean bool) { + value = bool ? TRUE : FALSE; } - /** - * Creates a new JsonArray that contains the JSON representations of the given - * double values. - * - * @param values the values to be included in the new JSON array - * @return a new JSON array that contains the given values - */ - public static JsonArray array(double... values) { - if (values == null) { - throw new NullPointerException("values is null"); - } - JsonArray array = new JsonArray(); - for (double value : values) { - array.add(value); - } - return array; + @Override + public void endString(String string) { + value = new JsonString(string); } - /** - * Creates a new JsonArray that contains the JSON representations of the given - * boolean values. - * - * @param values the values to be included in the new JSON array - * @return a new JSON array that contains the given values - */ - public static JsonArray array(boolean... values) { - if (values == null) { - throw new NullPointerException("values is null"); - } - JsonArray array = new JsonArray(); - for (boolean value : values) { - array.add(value); - } - return array; + @Override + public void endNumber(String string) { + value = new JsonNumber(string); } - /** - * Creates a new JsonArray that contains the JSON representations of the given strings. - * - * @param strings the strings to be included in the new JSON array - * @return a new JSON array that contains the given strings - */ - public static JsonArray array(String... strings) { - if (strings == null) { - throw new NullPointerException("values is null"); - } - JsonArray array = new JsonArray(); - for (String value : strings) { - array.add(value); - } - return array; + @Override + public void endArray(JsonArray array) { + value = array; } - /** - * Creates a new empty JsonObject. This is equivalent to creating a new JsonObject using the - * constructor. - * - * @return a new empty JSON object - */ - public static JsonObject object() { - return new JsonObject(); + @Override + public void endObject(JsonObject object) { + value = object; } - /** - * Parses the given input string as JSON. The input must contain a valid JSON value, optionally - * padded with whitespace. - * - * @param string the input string, must be valid JSON - * @return a value that represents the parsed JSON - * @throws ParseException if the input is not valid JSON - */ - public static JsonValue parse(String string) { - if (string == null) { - throw new NullPointerException("string is null"); - } - try { - return new JsonParser(string).parse(); - } catch (IOException exception) { - // JsonParser does not throw IOException for String - throw new RuntimeException(exception); - } + @Override + public void endArrayValue(JsonArray array) { + array.add(value); } - /** - * Reads the entire input stream from the given reader and parses it as JSON. The input must - * contain a valid JSON value, optionally padded with whitespace. - *

- * Characters are read in chunks and buffered internally, therefore wrapping an existing reader - * in an additional BufferedReader does not improve reading - * performance. - *

- * - * @param reader the reader to read the JSON value from - * @return a value that represents the parsed JSON - * @throws IOException if an I/O error occurs in the reader - * @throws ParseException if the input is not valid JSON - */ - public static JsonValue parse(Reader reader) throws IOException { - if (reader == null) { - throw new NullPointerException("reader is null"); - } - return new JsonParser(reader).parse(); + @Override + public void endObjectValue(JsonObject object, String name) { + object.add(name, value); } - private static String cutOffPointZero(String string) { - if (string.endsWith(".0")) { - return string.substring(0, string.length() - 2); - } - return string; + JsonValue getValue() { + return value; } + } + } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java b/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java index 1f3e13a3..fc812025 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java @@ -22,6 +22,7 @@ package io.github.jopenlibs.vault.json; import java.io.IOException; +import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -58,376 +59,453 @@ * This class is not supposed to be extended by clients. *

*/ -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID public class JsonArray extends JsonValue implements Iterable { - private final List values; - - /** - * Creates a new empty JsonArray. - */ - public JsonArray() { - values = new ArrayList<>(); - } - - /** - * Creates a new JsonArray with the contents of the specified JSON array. - * - * @param array the JsonArray to get the initial contents from, must not be null - */ - public JsonArray(JsonArray array) { - this(array, false); - } - - private JsonArray(JsonArray array, boolean unmodifiable) { - if (array == null) { - throw new NullPointerException("array is null"); - } - if (unmodifiable) { - values = Collections.unmodifiableList(array.values); - } else { - values = new ArrayList<>(array.values); - } - } - - /** - * Returns an unmodifiable wrapper for the specified JsonArray. This method allows to provide - * read-only access to a JsonArray. - *

- * The returned JsonArray is backed by the given array and reflects subsequent changes. Attempts - * to modify the returned JsonArray result in an UnsupportedOperationException. - *

- * - * @param array the JsonArray for which an unmodifiable JsonArray is to be returned - * @return an unmodifiable view of the specified JsonArray - */ - public static JsonArray unmodifiableArray(JsonArray array) { - return new JsonArray(array, true); - } - - /** - * Appends the JSON representation of the specified int value to the end of this - * array. - * - * @param value the value to add to the array - * @return the array itself, to enable method chaining - */ - public JsonArray add(int value) { - values.add(Json.value(value)); - return this; - } - - /** - * Appends the JSON representation of the specified long value to the end of this - * array. - * - * @param value the value to add to the array - * @return the array itself, to enable method chaining - */ - public JsonArray add(long value) { - values.add(Json.value(value)); - return this; - } - - /** - * Appends the JSON representation of the specified float value to the end of this - * array. - * - * @param value the value to add to the array - * @return the array itself, to enable method chaining - */ - public JsonArray add(float value) { - values.add(Json.value(value)); - return this; - } - - /** - * Appends the JSON representation of the specified double value to the end of this - * array. - * - * @param value the value to add to the array - * @return the array itself, to enable method chaining - */ - public JsonArray add(double value) { - values.add(Json.value(value)); - return this; - } - - /** - * Appends the JSON representation of the specified boolean value to the end of - * this array. - * - * @param value the value to add to the array - * @return the array itself, to enable method chaining - */ - public JsonArray add(boolean value) { - values.add(Json.value(value)); - return this; - } - - /** - * Appends the JSON representation of the specified string to the end of this array. - * - * @param value the string to add to the array - * @return the array itself, to enable method chaining - */ - public JsonArray add(String value) { - values.add(Json.value(value)); - return this; - } - - /** - * Appends the specified JSON value to the end of this array. - * - * @param value the JsonValue to add to the array, must not be null - * @return the array itself, to enable method chaining - */ - public JsonArray add(JsonValue value) { - if (value == null) { - throw new NullPointerException("value is null"); - } - values.add(value); - return this; - } - - /** - * Replaces the element at the specified position in this array with the JSON representation of - * the specified int value. - * - * @param index the index of the array element to replace - * @param value the value to be stored at the specified array position - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, int value) { - values.set(index, Json.value(value)); - return this; - } - - /** - * Replaces the element at the specified position in this array with the JSON representation of - * the specified long value. - * - * @param index the index of the array element to replace - * @param value the value to be stored at the specified array position - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, long value) { - values.set(index, Json.value(value)); - return this; + private final List values; + + /** + * Creates a new empty JsonArray. + */ + public JsonArray() { + values = new ArrayList(); + } + + /** + * Creates a new JsonArray with the contents of the specified JSON array. + * + * @param array + * the JsonArray to get the initial contents from, must not be null + */ + public JsonArray(JsonArray array) { + this(array, false); + } + + private JsonArray(JsonArray array, boolean unmodifiable) { + if (array == null) { + throw new NullPointerException("array is null"); } - - /** - * Replaces the element at the specified position in this array with the JSON representation of - * the specified float value. - * - * @param index the index of the array element to replace - * @param value the value to be stored at the specified array position - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, float value) { - values.set(index, Json.value(value)); - return this; + if (unmodifiable) { + values = Collections.unmodifiableList(array.values); + } else { + values = new ArrayList(array.values); } - - /** - * Replaces the element at the specified position in this array with the JSON representation of - * the specified double value. - * - * @param index the index of the array element to replace - * @param value the value to be stored at the specified array position - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, double value) { - values.set(index, Json.value(value)); - return this; + } + + /** + * Reads a JSON array from the given reader. + *

+ * Characters are read in chunks and buffered internally, therefore wrapping an existing reader in + * an additional BufferedReader does not improve reading + * performance. + *

+ * + * @param reader + * the reader to read the JSON array from + * @return the JSON array that has been read + * @throws IOException + * if an I/O error occurs in the reader + * @throws ParseException + * if the input is not valid JSON + * @throws UnsupportedOperationException + * if the input does not contain a JSON array + * @deprecated Use {@link Json#parse(Reader)}{@link JsonValue#asArray() .asArray()} instead + */ + @Deprecated + public static JsonArray readFrom(Reader reader) throws IOException { + return JsonValue.readFrom(reader).asArray(); + } + + /** + * Reads a JSON array from the given string. + * + * @param string + * the string that contains the JSON array + * @return the JSON array that has been read + * @throws ParseException + * if the input is not valid JSON + * @throws UnsupportedOperationException + * if the input does not contain a JSON array + * @deprecated Use {@link Json#parse(String)}{@link JsonValue#asArray() .asArray()} instead + */ + @Deprecated + public static JsonArray readFrom(String string) { + return JsonValue.readFrom(string).asArray(); + } + + /** + * Returns an unmodifiable wrapper for the specified JsonArray. This method allows to provide + * read-only access to a JsonArray. + *

+ * The returned JsonArray is backed by the given array and reflects subsequent changes. Attempts + * to modify the returned JsonArray result in an UnsupportedOperationException. + *

+ * + * @param array + * the JsonArray for which an unmodifiable JsonArray is to be returned + * @return an unmodifiable view of the specified JsonArray + */ + public static JsonArray unmodifiableArray(JsonArray array) { + return new JsonArray(array, true); + } + + /** + * Appends the JSON representation of the specified int value to the end of this + * array. + * + * @param value + * the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(int value) { + values.add(Json.value(value)); + return this; + } + + /** + * Appends the JSON representation of the specified long value to the end of this + * array. + * + * @param value + * the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(long value) { + values.add(Json.value(value)); + return this; + } + + /** + * Appends the JSON representation of the specified float value to the end of this + * array. + * + * @param value + * the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(float value) { + values.add(Json.value(value)); + return this; + } + + /** + * Appends the JSON representation of the specified double value to the end of this + * array. + * + * @param value + * the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(double value) { + values.add(Json.value(value)); + return this; + } + + /** + * Appends the JSON representation of the specified boolean value to the end of this + * array. + * + * @param value + * the value to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(boolean value) { + values.add(Json.value(value)); + return this; + } + + /** + * Appends the JSON representation of the specified string to the end of this array. + * + * @param value + * the string to add to the array + * @return the array itself, to enable method chaining + */ + public JsonArray add(String value) { + values.add(Json.value(value)); + return this; + } + + /** + * Appends the specified JSON value to the end of this array. + * + * @param value + * the JsonValue to add to the array, must not be null + * @return the array itself, to enable method chaining + */ + public JsonArray add(JsonValue value) { + if (value == null) { + throw new NullPointerException("value is null"); } - - /** - * Replaces the element at the specified position in this array with the JSON representation of - * the specified boolean value. - * - * @param index the index of the array element to replace - * @param value the value to be stored at the specified array position - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, boolean value) { - values.set(index, Json.value(value)); - return this; + values.add(value); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified int value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, int value) { + values.set(index, Json.value(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified long value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, long value) { + values.set(index, Json.value(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified float value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, float value) { + values.set(index, Json.value(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified double value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, double value) { + values.set(index, Json.value(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified boolean value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, boolean value) { + values.set(index, Json.value(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the JSON representation of + * the specified string. + * + * @param index + * the index of the array element to replace + * @param value + * the string to be stored at the specified array position + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, String value) { + values.set(index, Json.value(value)); + return this; + } + + /** + * Replaces the element at the specified position in this array with the specified JSON value. + * + * @param index + * the index of the array element to replace + * @param value + * the value to be stored at the specified array position, must not be null + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray set(int index, JsonValue value) { + if (value == null) { + throw new NullPointerException("value is null"); } - - /** - * Replaces the element at the specified position in this array with the JSON representation of - * the specified string. - * - * @param index the index of the array element to replace - * @param value the string to be stored at the specified array position - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, String value) { - values.set(index, Json.value(value)); - return this; + values.set(index, value); + return this; + } + + /** + * Removes the element at the specified index from this array. + * + * @param index + * the index of the element to remove + * @return the array itself, to enable method chaining + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonArray remove(int index) { + values.remove(index); + return this; + } + + /** + * Returns the number of elements in this array. + * + * @return the number of elements in this array + */ + public int size() { + return values.size(); + } + + /** + * Returns true if this array contains no elements. + * + * @return true if this array contains no elements + */ + public boolean isEmpty() { + return values.isEmpty(); + } + + /** + * Returns the value of the element at the specified position in this array. + * + * @param index + * the index of the array element to return + * @return the value of the element at the specified position + * @throws IndexOutOfBoundsException + * if the index is out of range, i.e. index < 0 or + * index >= size + */ + public JsonValue get(int index) { + return values.get(index); + } + + /** + * Returns a list of the values in this array in document order. The returned list is backed by + * this array and will reflect subsequent changes. It cannot be used to modify this array. + * Attempts to modify the returned list will result in an exception. + * + * @return a list of the values in this array + */ + public List values() { + return Collections.unmodifiableList(values); + } + + /** + * Returns an iterator over the values of this array in document order. The returned iterator + * cannot be used to modify this array. + * + * @return an iterator over the values of this array + */ + public Iterator iterator() { + final Iterator iterator = values.iterator(); + return new Iterator() { + + public boolean hasNext() { + return iterator.hasNext(); + } + + public JsonValue next() { + return iterator.next(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeArrayOpen(); + Iterator iterator = iterator(); + if (iterator.hasNext()) { + iterator.next().write(writer); + while (iterator.hasNext()) { + writer.writeArraySeparator(); + iterator.next().write(writer); + } } - - /** - * Replaces the element at the specified position in this array with the specified JSON value. - * - * @param index the index of the array element to replace - * @param value the value to be stored at the specified array position, must not be - * null - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray set(int index, JsonValue value) { - if (value == null) { - throw new NullPointerException("value is null"); - } - values.set(index, value); - return this; + writer.writeArrayClose(); + } + + @Override + public boolean isArray() { + return true; + } + + @Override + public JsonArray asArray() { + return this; + } + + @Override + public int hashCode() { + return values.hashCode(); + } + + + /** + * Indicates whether a given object is "equal to" this JsonArray. An object is considered equal + * if it is also a JsonArray and both arrays contain the same list of values. + *

+ * If two JsonArrays are equal, they will also produce the same JSON output. + *

+ * + * @param object + * the object to be compared with this JsonArray + * @return true if the specified object is equal to this JsonArray, false + * otherwise + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; } - - /** - * Removes the element at the specified index from this array. - * - * @param index the index of the element to remove - * @return the array itself, to enable method chaining - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonArray remove(int index) { - values.remove(index); - return this; + if (object == null) { + return false; } - - /** - * Returns the number of elements in this array. - * - * @return the number of elements in this array - */ - public int size() { - return values.size(); - } - - /** - * Returns true if this array contains no elements. - * - * @return true if this array contains no elements - */ - public boolean isEmpty() { - return values.isEmpty(); - } - - /** - * Returns the value of the element at the specified position in this array. - * - * @param index the index of the array element to return - * @return the value of the element at the specified position - * @throws IndexOutOfBoundsException if the index is out of range, i.e. index < - * 0 or - * index >= size - */ - public JsonValue get(int index) { - return values.get(index); - } - - /** - * Returns a list of the values in this array in document order. The returned list is backed by - * this array and will reflect subsequent changes. It cannot be used to modify this array. - * Attempts to modify the returned list will result in an exception. - * - * @return a list of the values in this array - */ - public List values() { - return Collections.unmodifiableList(values); - } - - /** - * Returns an iterator over the values of this array in document order. The returned iterator - * cannot be used to modify this array. - * - * @return an iterator over the values of this array - */ - public Iterator iterator() { - final Iterator iterator = values.iterator(); - return new Iterator() { - - public boolean hasNext() { - return iterator.hasNext(); - } - - public JsonValue next() { - return iterator.next(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - void write(JsonWriter writer) throws IOException { - writer.writeArrayOpen(); - Iterator iterator = iterator(); - boolean first = true; - while (iterator.hasNext()) { - if (!first) { - writer.writeArraySeparator(); - } - iterator.next().write(writer); - first = false; - } - writer.writeArrayClose(); - } - - @Override - public boolean isArray() { - return true; - } - - @Override - public JsonArray asArray() { - return this; - } - - @Override - public int hashCode() { - return values.hashCode(); - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object == null) { - return false; - } - if (getClass() != object.getClass()) { - return false; - } - JsonArray other = (JsonArray) object; - return values.equals(other.values); + if (getClass() != object.getClass()) { + return false; } + JsonArray other = (JsonArray)object; + return values.equals(other.values); + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonHandler.java b/src/main/java/io/github/jopenlibs/vault/json/JsonHandler.java new file mode 100644 index 00000000..8b73cafe --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonHandler.java @@ -0,0 +1,263 @@ +/******************************************************************************* + * Copyright (c) 2016 EclipseSource. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ +package io.github.jopenlibs.vault.json; + + +/** + * A handler for parser events. Instances of this class can be given to a {@link JsonParser}. The + * parser will then call the methods of the given handler while reading the input. + *

+ * The default implementations of these methods do nothing. Subclasses may override only those + * methods they are interested in. They can use getLocation() to access the current + * character position of the parser at any point. The start* methods will be called + * while the location points to the first character of the parsed element. The end* + * methods will be called while the location points to the character position that directly follows + * the last character of the parsed element. Example: + *

+ * + *
+ * ["lorem ipsum"]
+ *  ^            ^
+ *  startString  endString
+ * 
+ *

+ * Subclasses that build an object representation of the parsed JSON can return arbitrary handler + * objects for JSON arrays and JSON objects in {@link #startArray()} and {@link #startObject()}. + * These handler objects will then be provided in all subsequent parser events for this particular + * array or object. They can be used to keep track the elements of a JSON array or object. + *

+ * + * @param + * The type of handlers used for JSON arrays + * @param + * The type of handlers used for JSON objects + * @see JsonParser + */ +public abstract class JsonHandler { + + JsonParser parser; + + /** + * Returns the current parser location. + * + * @return the current parser location + */ + protected Location getLocation() { + return parser.getLocation(); + } + + /** + * Indicates the beginning of a null literal in the JSON input. This method will be + * called when reading the first character of the literal. + */ + public void startNull() { + } + + /** + * Indicates the end of a null literal in the JSON input. This method will be called + * after reading the last character of the literal. + */ + public void endNull() { + } + + /** + * Indicates the beginning of a boolean literal (true or false) in the + * JSON input. This method will be called when reading the first character of the literal. + */ + public void startBoolean() { + } + + /** + * Indicates the end of a boolean literal (true or false) in the JSON + * input. This method will be called after reading the last character of the literal. + * + * @param value + * the parsed boolean value + */ + public void endBoolean(boolean value) { + } + + /** + * Indicates the beginning of a string in the JSON input. This method will be called when reading + * the opening double quote character ('"'). + */ + public void startString() { + } + + /** + * Indicates the end of a string in the JSON input. This method will be called after reading the + * closing double quote character ('"'). + * + * @param string + * the parsed string + */ + public void endString(String string) { + } + + /** + * Indicates the beginning of a number in the JSON input. This method will be called when reading + * the first character of the number. + */ + public void startNumber() { + } + + /** + * Indicates the end of a number in the JSON input. This method will be called after reading the + * last character of the number. + * + * @param string + * the parsed number string + */ + public void endNumber(String string) { + } + + /** + * Indicates the beginning of an array in the JSON input. This method will be called when reading + * the opening square bracket character ('['). + *

+ * This method may return an object to handle subsequent parser events for this array. This array + * handler will then be provided in all calls to {@link #startArrayValue(Object) + * startArrayValue()}, {@link #endArrayValue(Object) endArrayValue()}, and + * {@link #endArray(Object) endArray()} for this array. + *

+ * + * @return a handler for this array, or null if not needed + */ + public A startArray() { + return null; + } + + /** + * Indicates the end of an array in the JSON input. This method will be called after reading the + * closing square bracket character (']'). + * + * @param array + * the array handler returned from {@link #startArray()}, or null if not + * provided + */ + public void endArray(A array) { + } + + /** + * Indicates the beginning of an array element in the JSON input. This method will be called when + * reading the first character of the element, just before the call to the start + * method for the specific element type ({@link #startString()}, {@link #startNumber()}, etc.). + * + * @param array + * the array handler returned from {@link #startArray()}, or null if not + * provided + */ + public void startArrayValue(A array) { + } + + /** + * Indicates the end of an array element in the JSON input. This method will be called after + * reading the last character of the element value, just after the end method for the + * specific element type (like {@link #endString(String) endString()}, {@link #endNumber(String) + * endNumber()}, etc.). + * + * @param array + * the array handler returned from {@link #startArray()}, or null if not + * provided + */ + public void endArrayValue(A array) { + } + + /** + * Indicates the beginning of an object in the JSON input. This method will be called when reading + * the opening curly bracket character ('{'). + *

+ * This method may return an object to handle subsequent parser events for this object. This + * object handler will be provided in all calls to {@link #startObjectName(Object) + * startObjectName()}, {@link #endObjectName(Object, String) endObjectName()}, + * {@link #startObjectValue(Object, String) startObjectValue()}, + * {@link #endObjectValue(Object, String) endObjectValue()}, and {@link #endObject(Object) + * endObject()} for this object. + *

+ * + * @return a handler for this object, or null if not needed + */ + public O startObject() { + return null; + } + + /** + * Indicates the end of an object in the JSON input. This method will be called after reading the + * closing curly bracket character ('}'). + * + * @param object + * the object handler returned from {@link #startObject()}, or null if not provided + */ + public void endObject(O object) { + } + + /** + * Indicates the beginning of the name of an object member in the JSON input. This method will be + * called when reading the opening quote character ('"') of the member name. + * + * @param object + * the object handler returned from {@link #startObject()}, or null if not + * provided + */ + public void startObjectName(O object) { + } + + /** + * Indicates the end of an object member name in the JSON input. This method will be called after + * reading the closing quote character ('"') of the member name. + * + * @param object + * the object handler returned from {@link #startObject()}, or null if not provided + * @param name + * the parsed member name + */ + public void endObjectName(O object, String name) { + } + + /** + * Indicates the beginning of the name of an object member in the JSON input. This method will be + * called when reading the opening quote character ('"') of the member name. + * + * @param object + * the object handler returned from {@link #startObject()}, or null if not + * provided + * @param name + * the member name + */ + public void startObjectValue(O object, String name) { + } + + /** + * Indicates the end of an object member value in the JSON input. This method will be called after + * reading the last character of the member value, just after the end method for the + * specific member type (like {@link #endString(String) endString()}, {@link #endNumber(String) + * endNumber()}, etc.). + * + * @param object + * the object handler returned from {@link #startObject()}, or null if not provided + * @param name + * the parsed member name + */ + public void endObjectValue(O object, String name) { + } + +} diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonLiteral.java b/src/main/java/io/github/jopenlibs/vault/json/JsonLiteral.java index e41a2eee..d03b6413 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonLiteral.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonLiteral.java @@ -24,74 +24,74 @@ import java.io.IOException; -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID class JsonLiteral extends JsonValue { - private final String value; - private final boolean isNull; - private final boolean isTrue; - private final boolean isFalse; + private final String value; + private final boolean isNull; + private final boolean isTrue; + private final boolean isFalse; - JsonLiteral(String value) { - this.value = value; - isNull = "null".equals(value); - isTrue = "true".equals(value); - isFalse = "false".equals(value); - } + JsonLiteral(String value) { + this.value = value; + isNull = "null".equals(value); + isTrue = "true".equals(value); + isFalse = "false".equals(value); + } - @Override - void write(JsonWriter writer) throws IOException { - writer.writeLiteral(value); - } + @Override + void write(JsonWriter writer) throws IOException { + writer.writeLiteral(value); + } - @Override - public String toString() { - return value; - } + @Override + public String toString() { + return value; + } - @Override - public int hashCode() { - return value.hashCode(); - } + @Override + public int hashCode() { + return value.hashCode(); + } - @Override - public boolean isNull() { - return isNull; - } + @Override + public boolean isNull() { + return isNull; + } - @Override - public boolean isTrue() { - return isTrue; - } + @Override + public boolean isTrue() { + return isTrue; + } - @Override - public boolean isFalse() { - return isFalse; - } + @Override + public boolean isFalse() { + return isFalse; + } - @Override - public boolean isBoolean() { - return isTrue || isFalse; - } + @Override + public boolean isBoolean() { + return isTrue || isFalse; + } - @Override - public boolean asBoolean() { - return isNull ? super.asBoolean() : isTrue; - } + @Override + public boolean asBoolean() { + return isNull ? super.asBoolean() : isTrue; + } - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object == null) { - return false; - } - if (getClass() != object.getClass()) { - return false; - } - JsonLiteral other = (JsonLiteral) object; - return value.equals(other.value); + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; } + JsonLiteral other = (JsonLiteral)object; + return value.equals(other.value); + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonNumber.java b/src/main/java/io/github/jopenlibs/vault/json/JsonNumber.java index e2873e86..f63f38f9 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonNumber.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonNumber.java @@ -24,71 +24,71 @@ import java.io.IOException; -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID class JsonNumber extends JsonValue { - private final String string; + private final String string; - JsonNumber(String string) { - if (string == null) { - throw new NullPointerException("string is null"); - } - this.string = string; + JsonNumber(String string) { + if (string == null) { + throw new NullPointerException("string is null"); } + this.string = string; + } - @Override - public String toString() { - return string; - } + @Override + public String toString() { + return string; + } - @Override - void write(JsonWriter writer) throws IOException { - writer.writeNumber(string); - } + @Override + void write(JsonWriter writer) throws IOException { + writer.writeNumber(string); + } - @Override - public boolean isNumber() { - return true; - } + @Override + public boolean isNumber() { + return true; + } - @Override - public int asInt() { - return Integer.parseInt(string, 10); - } + @Override + public int asInt() { + return Integer.parseInt(string, 10); + } - @Override - public long asLong() { - return Long.parseLong(string, 10); - } + @Override + public long asLong() { + return Long.parseLong(string, 10); + } - @Override - public float asFloat() { - return Float.parseFloat(string); - } + @Override + public float asFloat() { + return Float.parseFloat(string); + } - @Override - public double asDouble() { - return Double.parseDouble(string); - } + @Override + public double asDouble() { + return Double.parseDouble(string); + } - @Override - public int hashCode() { - return string.hashCode(); - } + @Override + public int hashCode() { + return string.hashCode(); + } - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object == null) { - return false; - } - if (getClass() != object.getClass()) { - return false; - } - JsonNumber other = (JsonNumber) object; - return string.equals(other.string); + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; } + JsonNumber other = (JsonNumber)object; + return string.equals(other.string); + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java b/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java index 90f66737..c8c95a28 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java @@ -24,6 +24,7 @@ import io.github.jopenlibs.vault.json.JsonObject.Member; import java.io.IOException; import java.io.ObjectInputStream; +import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -67,923 +68,866 @@ * This class is not supposed to be extended by clients. *

*/ -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID public class JsonObject extends JsonValue implements Iterable { - private final List names; - private final List values; - private transient HashIndexTable table; + private final List names; + private final List values; + private transient HashIndexTable table; + + /** + * Creates a new empty JsonObject. + */ + public JsonObject() { + names = new ArrayList(); + values = new ArrayList(); + table = new HashIndexTable(); + } + + /** + * Creates a new JsonObject, initialized with the contents of the specified JSON object. + * + * @param object + * the JSON object to get the initial contents from, must not be null + */ + public JsonObject(JsonObject object) { + this(object, false); + } + + private JsonObject(JsonObject object, boolean unmodifiable) { + if (object == null) { + throw new NullPointerException("object is null"); + } + if (unmodifiable) { + names = Collections.unmodifiableList(object.names); + values = Collections.unmodifiableList(object.values); + } else { + names = new ArrayList(object.names); + values = new ArrayList(object.values); + } + table = new HashIndexTable(); + updateHashIndex(); + } + + /** + * Reads a JSON object from the given reader. + *

+ * Characters are read in chunks and buffered internally, therefore wrapping an existing reader in + * an additional BufferedReader does not improve reading + * performance. + *

+ * + * @param reader + * the reader to read the JSON object from + * @return the JSON object that has been read + * @throws IOException + * if an I/O error occurs in the reader + * @throws ParseException + * if the input is not valid JSON + * @throws UnsupportedOperationException + * if the input does not contain a JSON object + * @deprecated Use {@link Json#parse(Reader)}{@link JsonValue#asObject() .asObject()} instead + */ + @Deprecated + public static JsonObject readFrom(Reader reader) throws IOException { + return JsonValue.readFrom(reader).asObject(); + } + + /** + * Reads a JSON object from the given string. + * + * @param string + * the string that contains the JSON object + * @return the JSON object that has been read + * @throws ParseException + * if the input is not valid JSON + * @throws UnsupportedOperationException + * if the input does not contain a JSON object + * @deprecated Use {@link Json#parse(String)}{@link JsonValue#asObject() .asObject()} instead + */ + @Deprecated + public static JsonObject readFrom(String string) { + return JsonValue.readFrom(string).asObject(); + } + + /** + * Returns an unmodifiable JsonObject for the specified one. This method allows to provide + * read-only access to a JsonObject. + *

+ * The returned JsonObject is backed by the given object and reflect changes that happen to it. + * Attempts to modify the returned JsonObject result in an + * UnsupportedOperationException. + *

+ * + * @param object + * the JsonObject for which an unmodifiable JsonObject is to be returned + * @return an unmodifiable view of the specified JsonObject + */ + public static JsonObject unmodifiableObject(JsonObject object) { + return new JsonObject(object, true); + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified int value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, int value) { + add(name, Json.value(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified long value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, long value) { + add(name, Json.value(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified float value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, float value) { + add(name, Json.value(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified double value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, double value) { + add(name, Json.value(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified boolean value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, boolean value) { + add(name, Json.value(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the JSON + * representation of the specified string. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, String value) { + add(name, Json.value(value)); + return this; + } + + /** + * Appends a new member to the end of this object, with the specified name and the specified JSON + * value. + *

+ * This method does not prevent duplicate names. Calling this method with a name + * that already exists in the object will append another member with the same name. In order to + * replace existing members, use the method set(name, value) instead. However, + * add is much faster than set (because it does not need to + * search for existing members). Therefore add should be preferred when constructing new + * objects. + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add, must not be null + * @return the object itself, to enable method chaining + */ + public JsonObject add(String name, JsonValue value) { + if (name == null) { + throw new NullPointerException("name is null"); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + table.add(name, names.size()); + names.add(name); + values.add(value); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified int value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to replace + * @param value + * the value to set to the member + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, int value) { + set(name, Json.value(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified long value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to replace + * @param value + * the value to set to the member + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, long value) { + set(name, Json.value(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified float value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, float value) { + set(name, Json.value(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified double value. If this object does not contain a member with this name, a + * new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, double value) { + set(name, Json.value(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified boolean value. If this object does not contain a member with this name, + * a new member is added at the end of the object. If this object contains multiple members with + * this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, boolean value) { + set(name, Json.value(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the JSON representation of the + * specified string. If this object does not contain a member with this name, a new member is + * added at the end of the object. If this object contains multiple members with this name, only + * the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, String value) { + set(name, Json.value(value)); + return this; + } + + /** + * Sets the value of the member with the specified name to the specified JSON value. If this + * object does not contain a member with this name, a new member is added at the end of the + * object. If this object contains multiple members with this name, only the last one is changed. + *

+ * This method should only be used to modify existing objects. To fill a new + * object with members, the method add(name, value) should be preferred which is much + * faster (as it does not need to search for existing members). + *

+ * + * @param name + * the name of the member to add + * @param value + * the value of the member to add, must not be null + * @return the object itself, to enable method chaining + */ + public JsonObject set(String name, JsonValue value) { + if (name == null) { + throw new NullPointerException("name is null"); + } + if (value == null) { + throw new NullPointerException("value is null"); + } + int index = indexOf(name); + if (index != -1) { + values.set(index, value); + } else { + table.add(name, names.size()); + names.add(name); + values.add(value); + } + return this; + } + + /** + * Removes a member with the specified name from this object. If this object contains multiple + * members with the given name, only the last one is removed. If this object does not contain a + * member with the specified name, the object is not modified. + * + * @param name + * the name of the member to remove + * @return the object itself, to enable method chaining + */ + public JsonObject remove(String name) { + if (name == null) { + throw new NullPointerException("name is null"); + } + int index = indexOf(name); + if (index != -1) { + table.remove(index); + names.remove(index); + values.remove(index); + } + return this; + } + + /** + * Checks if a specified member is present as a child of this object. This will not test if + * this object contains the literal null, {@link JsonValue#isNull()} should be used + * for this purpose. + * + * @param name + * the name of the member to check for + * @return whether or not the member is present + */ + public boolean contains(String name) { + return names.contains(name); + } + + /** + * Copies all members of the specified object into this object. When the specified object contains + * members with names that also exist in this object, the existing values in this object will be + * replaced by the corresponding values in the specified object. + * + * @param object + * the object to merge + * @return the object itself, to enable method chaining + */ + public JsonObject merge(JsonObject object) { + if (object == null) { + throw new NullPointerException("object is null"); + } + for (Member member : object) { + this.set(member.name, member.value); + } + return this; + } + + /** + * Returns the value of the member with the specified name in this object. If this object contains + * multiple members with the given name, this method will return the last one. + * + * @param name + * the name of the member whose value is to be returned + * @return the value of the last member with the specified name, or null if this + * object does not contain a member with that name + */ + public JsonValue get(String name) { + if (name == null) { + throw new NullPointerException("name is null"); + } + int index = indexOf(name); + return index != -1 ? values.get(index) : null; + } + + /** + * Returns the int value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * int, an exception is thrown. + * + * @param name + * the name of the member whose value is to be returned + * @param defaultValue + * the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public int getInt(String name, int defaultValue) { + JsonValue value = get(name); + return value != null ? value.asInt() : defaultValue; + } + + /** + * Returns the long value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * long, an exception is thrown. + * + * @param name + * the name of the member whose value is to be returned + * @param defaultValue + * the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public long getLong(String name, long defaultValue) { + JsonValue value = get(name); + return value != null ? value.asLong() : defaultValue; + } + + /** + * Returns the float value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * float, an exception is thrown. + * + * @param name + * the name of the member whose value is to be returned + * @param defaultValue + * the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public float getFloat(String name, float defaultValue) { + JsonValue value = get(name); + return value != null ? value.asFloat() : defaultValue; + } + + /** + * Returns the double value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON number or if it cannot be interpreted as Java + * double, an exception is thrown. + * + * @param name + * the name of the member whose value is to be returned + * @param defaultValue + * the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public double getDouble(String name, double defaultValue) { + JsonValue value = get(name); + return value != null ? value.asDouble() : defaultValue; + } + + /** + * Returns the boolean value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one will be picked. If this + * member's value does not represent a JSON true or false value, an + * exception is thrown. + * + * @param name + * the name of the member whose value is to be returned + * @param defaultValue + * the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public boolean getBoolean(String name, boolean defaultValue) { + JsonValue value = get(name); + return value != null ? value.asBoolean() : defaultValue; + } + + /** + * Returns the String value of the member with the specified name in this object. If + * this object does not contain a member with this name, the given default value is returned. If + * this object contains multiple members with the given name, the last one is picked. If this + * member's value does not represent a JSON string, an exception is thrown. + * + * @param name + * the name of the member whose value is to be returned + * @param defaultValue + * the value to be returned if the requested member is missing + * @return the value of the last member with the specified name, or the given default value if + * this object does not contain a member with that name + */ + public String getString(String name, String defaultValue) { + JsonValue value = get(name); + return value != null ? value.asString() : defaultValue; + } + + /** + * Returns the number of members (name/value pairs) in this object. + * + * @return the number of members in this object + */ + public int size() { + return names.size(); + } + + /** + * Returns true if this object contains no members. + * + * @return true if this object contains no members + */ + public boolean isEmpty() { + return names.isEmpty(); + } + + /** + * Returns a list of the names in this object in document order. The returned list is backed by + * this object and will reflect subsequent changes. It cannot be used to modify this object. + * Attempts to modify the returned list will result in an exception. + * + * @return a list of the names in this object + */ + public List names() { + return Collections.unmodifiableList(names); + } + + /** + * Returns an iterator over the members of this object in document order. The returned iterator + * cannot be used to modify this object. + * + * @return an iterator over the members of this object + */ + public Iterator iterator() { + final Iterator namesIterator = names.iterator(); + final Iterator valuesIterator = values.iterator(); + return new Iterator() { + + public boolean hasNext() { + return namesIterator.hasNext(); + } + + public Member next() { + String name = namesIterator.next(); + JsonValue value = valuesIterator.next(); + return new Member(name, value); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + }; + } + + @Override + void write(JsonWriter writer) throws IOException { + writer.writeObjectOpen(); + Iterator namesIterator = names.iterator(); + Iterator valuesIterator = values.iterator(); + if (namesIterator.hasNext()) { + writer.writeMemberName(namesIterator.next()); + writer.writeMemberSeparator(); + valuesIterator.next().write(writer); + while (namesIterator.hasNext()) { + writer.writeObjectSeparator(); + writer.writeMemberName(namesIterator.next()); + writer.writeMemberSeparator(); + valuesIterator.next().write(writer); + } + } + writer.writeObjectClose(); + } + + @Override + public boolean isObject() { + return true; + } + + @Override + public JsonObject asObject() { + return this; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + names.hashCode(); + result = 31 * result + values.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + JsonObject other = (JsonObject)obj; + return names.equals(other.names) && values.equals(other.values); + } + + int indexOf(String name) { + int index = table.get(name); + if (index != -1 && name.equals(names.get(index))) { + return index; + } + return names.lastIndexOf(name); + } + + private synchronized void readObject(ObjectInputStream inputStream) + throws IOException, ClassNotFoundException + { + inputStream.defaultReadObject(); + table = new HashIndexTable(); + updateHashIndex(); + } - /** - * Creates a new empty JsonObject. - */ - public JsonObject() { - names = new ArrayList<>(); - values = new ArrayList<>(); - table = new HashIndexTable(); - } - - /** - * Creates a new JsonObject, initialized with the contents of the specified JSON object. - * - * @param object the JSON object to get the initial contents from, must not be - * null - */ - public JsonObject(JsonObject object) { - this(object, false); - } - - private JsonObject(JsonObject object, boolean unmodifiable) { - if (object == null) { - throw new NullPointerException("object is null"); - } - if (unmodifiable) { - names = Collections.unmodifiableList(object.names); - values = Collections.unmodifiableList(object.values); - } else { - names = new ArrayList<>(object.names); - values = new ArrayList<>(object.values); - } - table = new HashIndexTable(); - updateHashIndex(); - } - - /** - * Returns an unmodifiable JsonObject for the specified one. This method allows to provide - * read-only access to a JsonObject. - *

- * The returned JsonObject is backed by the given object and reflect changes that happen to it. - * Attempts to modify the returned JsonObject result in an - * UnsupportedOperationException. - *

- * - * @param object the JsonObject for which an unmodifiable JsonObject is to be returned - * @return an unmodifiable view of the specified JsonObject - */ - public static JsonObject unmodifiableObject(JsonObject object) { - return new JsonObject(object, true); - } - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified int value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, int value) { - add(name, Json.value(value)); - return this; - } - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified int[] value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, int[] value) { - add(name, Json.value(value)); - return this; - } - - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified long value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, long value) { - add(name, Json.value(value)); - return this; - } - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified float value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, float value) { - add(name, Json.value(value)); - return this; - } - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified double value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, double value) { - add(name, Json.value(value)); - return this; - } - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified boolean value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, boolean value) { - add(name, Json.value(value)); - return this; - } - - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified string. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, String value) { - add(name, Json.value(value)); - return this; + private void updateHashIndex() { + int size = names.size(); + for (int i = 0; i < size; i++) { + table.add(names.get(i), i); } + } - /** - * Appends a new member to the end of this object, with the specified name and the JSON - * representation of the specified casted Object only if the value is not - * null. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject addIfNotNull(final String name, final Object value) { - if (value == null) { - return this; - } - if (value instanceof JsonValue) { - this.add(name, (JsonValue) value); - } else if (value instanceof Integer) { - this.add(name, (Integer) value); - } else if (value instanceof Long) { - this.add(name, (Long) value); - } else if (value instanceof Float) { - this.add(name, (Float) value); - } else if (value instanceof Double) { - this.add(name, (Double) value); - } else if (value instanceof Boolean) { - this.add(name, (Boolean) value); - } else if (value instanceof String) { - this.add(name, (String) value); - } - return this; - } + /** + * Represents a member of a JSON object, a pair of a name and a value. + */ + public static class Member { - /** - * Appends a new member to the end of this object, with the specified name and the specified - * JSON value. - *

- * This method does not prevent duplicate names. Calling this method with a - * name that already exists in the object will append another member with the same name. In - * order to replace existing members, use the method set(name, value) instead. - * However, - * add is much faster than set (because it does not need to - * search for existing members). Therefore add should be preferred when constructing - * new objects. - *

- * - * @param name the name of the member to add - * @param value the value of the member to add, must not be null - * @return the object itself, to enable method chaining - */ - public JsonObject add(String name, JsonValue value) { - if (name == null) { - throw new NullPointerException("name is null"); - } - if (value == null) { - throw new NullPointerException("value is null"); - } - table.add(name, names.size()); - names.add(name); - values.add(value); - return this; - } - - /** - * Sets the value of the member with the specified name to the JSON representation of the - * specified int value. If this object does not contain a member with this name, a - * new member is added at the end of the object. If this object contains multiple members with - * this name, only the last one is changed. - *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). - *

- * - * @param name the name of the member to replace - * @param value the value to set to the member - * @return the object itself, to enable method chaining - */ - public JsonObject set(String name, int value) { - set(name, Json.value(value)); - return this; - } + private final String name; + private final JsonValue value; - /** - * Sets the value of the member with the specified name to the JSON representation of the - * specified long value. If this object does not contain a member with this name, a - * new member is added at the end of the object. If this object contains multiple members with - * this name, only the last one is changed. - *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). - *

- * - * @param name the name of the member to replace - * @param value the value to set to the member - * @return the object itself, to enable method chaining - */ - public JsonObject set(String name, long value) { - set(name, Json.value(value)); - return this; - } - - /** - * Sets the value of the member with the specified name to the JSON representation of the - * specified float value. If this object does not contain a member with this name, - * a new member is added at the end of the object. If this object contains multiple members with - * this name, only the last one is changed. - *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject set(String name, float value) { - set(name, Json.value(value)); - return this; + Member(String name, JsonValue value) { + this.name = name; + this.value = value; } /** - * Sets the value of the member with the specified name to the JSON representation of the - * specified double value. If this object does not contain a member with this name, - * a new member is added at the end of the object. If this object contains multiple members with - * this name, only the last one is changed. - *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). - *

+ * Returns the name of this member. * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining + * @return the name of this member, never null */ - public JsonObject set(String name, double value) { - set(name, Json.value(value)); - return this; + public String getName() { + return name; } /** - * Sets the value of the member with the specified name to the JSON representation of the - * specified boolean value. If this object does not contain a member with this - * name, a new member is added at the end of the object. If this object contains multiple - * members with this name, only the last one is changed. - *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). - *

+ * Returns the value of this member. * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining + * @return the value of this member, never null */ - public JsonObject set(String name, boolean value) { - set(name, Json.value(value)); - return this; + public JsonValue getValue() { + return value; } - /** - * Sets the value of the member with the specified name to the JSON representation of the - * specified string. If this object does not contain a member with this name, a new member is - * added at the end of the object. If this object contains multiple members with this name, only - * the last one is changed. - *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). - *

- * - * @param name the name of the member to add - * @param value the value of the member to add - * @return the object itself, to enable method chaining - */ - public JsonObject set(String name, String value) { - set(name, Json.value(value)); - return this; + @Override + public int hashCode() { + int result = 1; + result = 31 * result + name.hashCode(); + result = 31 * result + value.hashCode(); + return result; } /** - * Sets the value of the member with the specified name to the specified JSON value. If this - * object does not contain a member with this name, a new member is added at the end of the - * object. If this object contains multiple members with this name, only the last one is - * changed. + * Indicates whether a given object is "equal to" this JsonObject. An object is considered equal + * if it is also a JsonObject and both objects contain the same members in + * the same order. *

- * This method should only be used to modify existing objects. To fill a new - * object with members, the method add(name, value) should be preferred which is - * much faster (as it does not need to search for existing members). + * If two JsonObjects are equal, they will also produce the same JSON output. *

* - * @param name the name of the member to add - * @param value the value of the member to add, must not be null - * @return the object itself, to enable method chaining + * @param object + * the object to be compared with this JsonObject + * @return true if the specified object is equal to this JsonObject, false + * otherwise */ - public JsonObject set(String name, JsonValue value) { - if (name == null) { - throw new NullPointerException("name is null"); - } - if (value == null) { - throw new NullPointerException("value is null"); - } - int index = indexOf(name); - if (index != -1) { - values.set(index, value); - } else { - table.add(name, names.size()); - names.add(name); - values.add(value); - } - return this; - } - - /** - * Removes a member with the specified name from this object. If this object contains multiple - * members with the given name, only the last one is removed. If this object does not contain a - * member with the specified name, the object is not modified. - * - * @param name the name of the member to remove - * @return the object itself, to enable method chaining - */ - public JsonObject remove(String name) { - if (name == null) { - throw new NullPointerException("name is null"); - } - int index = indexOf(name); - if (index != -1) { - table.remove(index); - names.remove(index); - values.remove(index); - } - return this; - } - - /** - * Copies all members of the specified object into this object. When the specified object - * contains members with names that also exist in this object, the existing values in this - * object will be replaced by the corresponding values in the specified object. - * - * @param object the object to merge - * @return the object itself, to enable method chaining - */ - public JsonObject merge(JsonObject object) { - if (object == null) { - throw new NullPointerException("object is null"); - } - for (Member member : object) { - this.set(member.name, member.value); - } - return this; - } - - /** - * Returns the value of the member with the specified name in this object. If this object - * contains multiple members with the given name, this method will return the last one. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this - * object does not contain a member with that name - */ - public JsonValue get(String name) { - if (name == null) { - throw new NullPointerException("name is null"); - } - int index = indexOf(name); - return index != -1 ? values.get(index) : null; - } - - /** - * Returns the Integer value of the member with the specified name in this object. - * If this object does not contain a member with this name, null - * is returned. If this object contains multiple members with the given name, the last one will - * be picked. If this member's value does not represent a JSON number or if it cannot be - * interpreted as Java - * int, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this object does not - * contain a member with that name - */ - public Integer getInt(String name) { - JsonValue value = get(name); - return value != null ? value.asInt() : null; - } - - /** - * Returns the int value of the member with the specified name in this object. If - * this object does not contain a member with this name, the given default value is returned. If - * this object contains multiple members with the given name, the last one will be picked. If - * this member's value does not represent a JSON number or if it cannot be interpreted as Java - * int, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @param defaultValue the value to be returned if the requested member is missing - * @return the value of the last member with the specified name, or the given default value if - * this object does not contain a member with that name - */ - public int getInt(String name, int defaultValue) { - JsonValue value = get(name); - return value != null ? value.asInt() : defaultValue; - } - - /** - * Returns the Long value of the member with the specified name in this object. If - * this object does not contain a member with this name, null is - * returned. If this object contains multiple members with the given name, the last one will be - * picked. If this member's value does not represent a JSON number or if it cannot be - * interpreted as Java - * long, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this object does not - * contain a member with that name - */ - public Long getLong(String name) { - JsonValue value = get(name); - return value != null ? value.asLong() : null; - } - - /** - * Returns the long value of the member with the specified name in this object. If - * this object does not contain a member with this name, the given default value is returned. If - * this object contains multiple members with the given name, the last one will be picked. If - * this member's value does not represent a JSON number or if it cannot be interpreted as Java - * long, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @param defaultValue the value to be returned if the requested member is missing - * @return the value of the last member with the specified name, or the given default value if - * this object does not contain a member with that name - */ - public long getLong(String name, long defaultValue) { - JsonValue value = get(name); - return value != null ? value.asLong() : defaultValue; - } - - /** - * Returns the Float value of the member with the specified name in this object. If - * this object does not contain a member with this name, null is - * returned. If this object contains multiple members with the given name, the last one will be - * picked. If this member's value does not represent a JSON number or if it cannot be - * interpreted as Java - * float, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this object does not - * contain a member with that name - */ - public Float getFloat(String name) { - JsonValue value = get(name); - return value != null ? value.asFloat() : null; - } - - /** - * Returns the float value of the member with the specified name in this object. If - * this object does not contain a member with this name, the given default value is returned. If - * this object contains multiple members with the given name, the last one will be picked. If - * this member's value does not represent a JSON number or if it cannot be interpreted as Java - * float, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @param defaultValue the value to be returned if the requested member is missing - * @return the value of the last member with the specified name, or the given default value if - * this object does not contain a member with that name - */ - public float getFloat(String name, float defaultValue) { - JsonValue value = get(name); - return value != null ? value.asFloat() : defaultValue; - } - - /** - * Returns the Double value of the member with the specified name in this object. - * If this object does not contain a member with this name, null - * is returned. If this object contains multiple members with the given name, the last one will - * be picked. If this member's value does not represent a JSON number or if it cannot be - * interpreted as Java - * double, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this object does not - * contain a member with that name - */ - public Double getDouble(String name) { - JsonValue value = get(name); - return value != null ? value.asDouble() : null; - } - - /** - * Returns the double value of the member with the specified name in this object. - * If this object does not contain a member with this name, the given default value is returned. - * If this object contains multiple members with the given name, the last one will be picked. If - * this member's value does not represent a JSON number or if it cannot be interpreted as Java - * double, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @param defaultValue the value to be returned if the requested member is missing - * @return the value of the last member with the specified name, or the given default value if - * this object does not contain a member with that name - */ - public double getDouble(String name, double defaultValue) { - JsonValue value = get(name); - return value != null ? value.asDouble() : defaultValue; - } - - /** - * Returns the Boolean value of the member with the specified name in this object. - * If this object does not contain a member with this name, null - * is returned. If this object contains multiple members with the given name, the last one will - * be picked. If this member's value does not represent a JSON true or - * false value, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this object does not - * contain a member with that name - */ - public Boolean getBoolean(String name) { - JsonValue value = get(name); - return value != null ? value.asBoolean() : null; - } - - /** - * Returns the boolean value of the member with the specified name in this object. - * If this object does not contain a member with this name, the given default value is returned. - * If this object contains multiple members with the given name, the last one will be picked. If - * this member's value does not represent a JSON true or false value, - * an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @param defaultValue the value to be returned if the requested member is missing - * @return the value of the last member with the specified name, or the given default value if - * this object does not contain a member with that name - */ - public boolean getBoolean(String name, boolean defaultValue) { - JsonValue value = get(name); - return value != null ? value.asBoolean() : defaultValue; - } - - /** - * Returns the String value of the member with the specified name in this object. - * If this object does not contain a member with this name, null - * is returned. If this object contains multiple members with the given name, the last one is - * picked. If this member's value does not represent a JSON string, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @return the value of the last member with the specified name, or null if this object does not - * contain a member with that name - */ - public String getString(String name) { - JsonValue value = get(name); - return value != null ? value.asString() : null; - } - - /** - * Returns the String value of the member with the specified name in this object. - * If this object does not contain a member with this name, the given default value is returned. - * If this object contains multiple members with the given name, the last one is picked. If this - * member's value does not represent a JSON string, an exception is thrown. - * - * @param name the name of the member whose value is to be returned - * @param defaultValue the value to be returned if the requested member is missing - * @return the value of the last member with the specified name, or the given default value if - * this object does not contain a member with that name - */ - public String getString(String name, String defaultValue) { - JsonValue value = get(name); - return value != null ? value.asString() : defaultValue; - } - - /** - * Returns the number of members (name/value pairs) in this object. - * - * @return the number of members in this object - */ - public int size() { - return names.size(); - } - - /** - * Returns true if this object contains no members. - * - * @return true if this object contains no members - */ - public boolean isEmpty() { - return names.isEmpty(); - } - - /** - * Returns a list of the names in this object in document order. The returned list is backed by - * this object and will reflect subsequent changes. It cannot be used to modify this object. - * Attempts to modify the returned list will result in an exception. - * - * @return a list of the names in this object - */ - public List names() { - return Collections.unmodifiableList(names); - } - - /** - * Returns an iterator over the members of this object in document order. The returned iterator - * cannot be used to modify this object. - * - * @return an iterator over the members of this object - */ - public Iterator iterator() { - final Iterator namesIterator = names.iterator(); - final Iterator valuesIterator = values.iterator(); - return new Iterator() { - - public boolean hasNext() { - return namesIterator.hasNext(); - } - - public Member next() { - String name = namesIterator.next(); - JsonValue value = valuesIterator.next(); - return new Member(name, value); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - - }; - } - - @Override - void write(JsonWriter writer) throws IOException { - writer.writeObjectOpen(); - Iterator namesIterator = names.iterator(); - Iterator valuesIterator = values.iterator(); - boolean first = true; - while (namesIterator.hasNext()) { - if (!first) { - writer.writeObjectSeparator(); - } - writer.writeMemberName(namesIterator.next()); - writer.writeMemberSeparator(); - valuesIterator.next().write(writer); - first = false; - } - writer.writeObjectClose(); - } - @Override - public boolean isObject() { + public boolean equals(Object object) { + if (this == object) { return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; + } + Member other = (Member)object; + return name.equals(other.name) && value.equals(other.value); } - @Override - public JsonObject asObject() { - return this; - } + } - @Override - public int hashCode() { - int result = 1; - result = 31 * result + names.hashCode(); - result = 31 * result + values.hashCode(); - return result; - } + static class HashIndexTable { - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - JsonObject other = (JsonObject) obj; - return names.equals(other.names) && values.equals(other.values); - } + private final byte[] hashTable = new byte[32]; // must be a power of two - int indexOf(String name) { - int index = table.get(name); - if (index != -1 && name.equals(names.get(index))) { - return index; - } - return names.lastIndexOf(name); + HashIndexTable() { } - private synchronized void readObject(ObjectInputStream inputStream) - throws IOException, ClassNotFoundException { - inputStream.defaultReadObject(); - table = new HashIndexTable(); - updateHashIndex(); + HashIndexTable(HashIndexTable original) { + System.arraycopy(original.hashTable, 0, hashTable, 0, hashTable.length); } - private void updateHashIndex() { - int size = names.size(); - for (int i = 0; i < size; i++) { - table.add(names.get(i), i); - } + void add(String name, int index) { + int slot = hashSlotFor(name); + if (index < 0xff) { + // increment by 1, 0 stands for empty + hashTable[slot] = (byte)(index + 1); + } else { + hashTable[slot] = 0; + } } - /** - * Represents a member of a JSON object, a pair of a name and a value. - */ - public static class Member { - - private final String name; - private final JsonValue value; - - Member(String name, JsonValue value) { - this.name = name; - this.value = value; - } - - /** - * Returns the name of this member. - * - * @return the name of this member, never null - */ - public String getName() { - return name; - } - - /** - * Returns the value of this member. - * - * @return the value of this member, never null - */ - public JsonValue getValue() { - return value; - } - - @Override - public int hashCode() { - int result = 1; - result = 31 * result + name.hashCode(); - result = 31 * result + value.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - Member other = (Member) obj; - return name.equals(other.name) && value.equals(other.value); + void remove(int index) { + for (int i = 0; i < hashTable.length; i++) { + if ((hashTable[i] & 0xff) == index + 1) { + hashTable[i] = 0; + } else if ((hashTable[i] & 0xff) > index + 1) { + hashTable[i]--; } - + } } - static class HashIndexTable { - - private final byte[] hashTable = new byte[32]; // must be a power of two - - public HashIndexTable() { - } - - public HashIndexTable(HashIndexTable original) { - System.arraycopy(original.hashTable, 0, hashTable, 0, hashTable.length); - } - - void add(String name, int index) { - int slot = hashSlotFor(name); - if (index < 0xff) { - // increment by 1, 0 stands for empty - hashTable[slot] = (byte) (index + 1); - } else { - hashTable[slot] = 0; - } - } - - void remove(int index) { - for (int i = 0; i < hashTable.length; i++) { - if (hashTable[i] == index + 1) { - hashTable[i] = 0; - } else if (hashTable[i] > index + 1) { - hashTable[i]--; - } - } - } - - int get(Object name) { - int slot = hashSlotFor(name); - // subtract 1, 0 stands for empty - return (hashTable[slot] & 0xff) - 1; - } - - private int hashSlotFor(Object element) { - return element.hashCode() & hashTable.length - 1; - } + int get(Object name) { + int slot = hashSlotFor(name); + // subtract 1, 0 stands for empty + return (hashTable[slot] & 0xff) - 1; + } + private int hashSlotFor(Object element) { + return element.hashCode() & hashTable.length - 1; } + } + } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonParser.java b/src/main/java/io/github/jopenlibs/vault/json/JsonParser.java index 458290f5..fed1be88 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonParser.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonParser.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2015 EclipseSource. + * Copyright (c) 2013, 2016 EclipseSource. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,376 +26,486 @@ import java.io.StringReader; -@SuppressWarnings("PMD") -class JsonParser { - - private static final int MIN_BUFFER_SIZE = 10; - private static final int DEFAULT_BUFFER_SIZE = 1024; - - private final Reader reader; - private final char[] buffer; - private int bufferOffset; - private int index; - private int fill; - private int line; - private int lineOffset; - private int current; - private StringBuilder captureBuffer; - private int captureStart; - - /* - * | bufferOffset - * v - * [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t] < input - * [l|m|n|o|p|q|r|s|t|?|?] < buffer - * ^ ^ - * | index fill - */ - - JsonParser(String string) { - this(new StringReader(string), - Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length()))); +/** + * A streaming parser for JSON text. The parser reports all events to a given handler. + */ +public class JsonParser { + + private static final int MAX_NESTING_LEVEL = 1000; + private static final int MIN_BUFFER_SIZE = 10; + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private final JsonHandler handler; + private Reader reader; + private char[] buffer; + private int bufferOffset; + private int index; + private int fill; + private int line; + private int lineOffset; + private int current; + private StringBuilder captureBuffer; + private int captureStart; + private int nestingLevel; + + /* + * | bufferOffset + * v + * [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t] < input + * [l|m|n|o|p|q|r|s|t|?|?] < buffer + * ^ ^ + * | index fill + */ + + /** + * Creates a new JsonParser with the given handler. The parser will report all parser events to + * this handler. + * + * @param handler + * the handler to process parser events + */ + @SuppressWarnings("unchecked") + public JsonParser(JsonHandler handler) { + if (handler == null) { + throw new NullPointerException("handler is null"); } - - JsonParser(Reader reader) { - this(reader, DEFAULT_BUFFER_SIZE); + this.handler = (JsonHandler)handler; + handler.parser = this; + } + + /** + * Parses the given input string. The input must contain a valid JSON value, optionally padded + * with whitespace. + * + * @param string + * the input string, must be valid JSON + * @throws ParseException + * if the input is not valid JSON + */ + public void parse(String string) { + if (string == null) { + throw new NullPointerException("string is null"); } - - JsonParser(Reader reader, int buffersize) { - this.reader = reader; - buffer = new char[buffersize]; - line = 1; - captureStart = -1; + int bufferSize = Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length())); + try { + parse(new StringReader(string), bufferSize); + } catch (IOException exception) { + // StringReader does not throw IOException + throw new RuntimeException(exception); } - - JsonValue parse() throws IOException { - read(); - skipWhiteSpace(); - JsonValue result = readValue(); - skipWhiteSpace(); - if (!isEndOfText()) { - throw error("Unexpected character"); - } - return result; - } - - private JsonValue readValue() throws IOException { - switch (current) { - case 'n': - return readNull(); - case 't': - return readTrue(); - case 'f': - return readFalse(); - case '"': - return readString(); - case '[': - return readArray(); - case '{': - return readObject(); - case '-': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - return readNumber(); - default: - throw expected("value"); - } + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input must contain a + * valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into a default-sized input buffer. Hence, wrapping a reader in an + * additional BufferedReader likely won't improve reading performance. + *

+ * + * @param reader + * the reader to read the input from + * @throws IOException + * if an I/O error occurs in the reader + * @throws ParseException + * if the input is not valid JSON + */ + public void parse(Reader reader) throws IOException { + parse(reader, DEFAULT_BUFFER_SIZE); + } + + /** + * Reads the entire input from the given reader and parses it as JSON. The input must contain a + * valid JSON value, optionally padded with whitespace. + *

+ * Characters are read in chunks into an input buffer of the given size. Hence, wrapping a reader + * in an additional BufferedReader likely won't improve reading performance. + *

+ * + * @param reader + * the reader to read the input from + * @param buffersize + * the size of the input buffer in chars + * @throws IOException + * if an I/O error occurs in the reader + * @throws ParseException + * if the input is not valid JSON + */ + public void parse(Reader reader, int buffersize) throws IOException { + if (reader == null) { + throw new NullPointerException("reader is null"); } - - private JsonArray readArray() throws IOException { - read(); - JsonArray array = new JsonArray(); - skipWhiteSpace(); - if (readChar(']')) { - return array; - } - do { - skipWhiteSpace(); - array.add(readValue()); - skipWhiteSpace(); - } while (readChar(',')); - if (!readChar(']')) { - throw expected("',' or ']'"); - } - return array; + if (buffersize <= 0) { + throw new IllegalArgumentException("buffersize is zero or negative"); } - - private JsonObject readObject() throws IOException { - read(); - JsonObject object = new JsonObject(); - skipWhiteSpace(); - if (readChar('}')) { - return object; - } - do { - skipWhiteSpace(); - String name = readName(); - skipWhiteSpace(); - if (!readChar(':')) { - throw expected("':'"); - } - skipWhiteSpace(); - object.add(name, readValue()); - skipWhiteSpace(); - } while (readChar(',')); - if (!readChar('}')) { - throw expected("',' or '}'"); - } - return object; + this.reader = reader; + buffer = new char[buffersize]; + bufferOffset = 0; + index = 0; + fill = 0; + line = 1; + lineOffset = 0; + current = 0; + captureStart = -1; + read(); + skipWhiteSpace(); + readValue(); + skipWhiteSpace(); + if (!isEndOfText()) { + throw error("Unexpected character"); } - - private String readName() throws IOException { - if (current != '"') { - throw expected("name"); - } - return readStringInternal(); + } + + private void readValue() throws IOException { + switch (current) { + case 'n': + readNull(); + break; + case 't': + readTrue(); + break; + case 'f': + readFalse(); + break; + case '"': + readString(); + break; + case '[': + readArray(); + break; + case '{': + readObject(); + break; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + readNumber(); + break; + default: + throw expected("value"); } + } - private JsonValue readNull() throws IOException { - read(); - readRequiredChar('u'); - readRequiredChar('l'); - readRequiredChar('l'); - return Json.NULL; + private void readArray() throws IOException { + Object array = handler.startArray(); + read(); + if (++nestingLevel > MAX_NESTING_LEVEL) { + throw error("Nesting too deep"); } - - private JsonValue readTrue() throws IOException { - read(); - readRequiredChar('r'); - readRequiredChar('u'); - readRequiredChar('e'); - return Json.TRUE; + skipWhiteSpace(); + if (readChar(']')) { + nestingLevel--; + handler.endArray(array); + return; } - - private JsonValue readFalse() throws IOException { - read(); - readRequiredChar('a'); - readRequiredChar('l'); - readRequiredChar('s'); - readRequiredChar('e'); - return Json.FALSE; + do { + skipWhiteSpace(); + handler.startArrayValue(array); + readValue(); + handler.endArrayValue(array); + skipWhiteSpace(); + } while (readChar(',')); + if (!readChar(']')) { + throw expected("',' or ']'"); } - - private void readRequiredChar(char ch) throws IOException { - if (!readChar(ch)) { - throw expected("'" + ch + "'"); - } + nestingLevel--; + handler.endArray(array); + } + + private void readObject() throws IOException { + Object object = handler.startObject(); + read(); + if (++nestingLevel > MAX_NESTING_LEVEL) { + throw error("Nesting too deep"); } - - private JsonValue readString() throws IOException { - return new JsonString(readStringInternal()); + skipWhiteSpace(); + if (readChar('}')) { + nestingLevel--; + handler.endObject(object); + return; } + do { + skipWhiteSpace(); + handler.startObjectName(object); + String name = readName(); + handler.endObjectName(object, name); + skipWhiteSpace(); + if (!readChar(':')) { + throw expected("':'"); + } + skipWhiteSpace(); + handler.startObjectValue(object, name); + readValue(); + handler.endObjectValue(object, name); + skipWhiteSpace(); + } while (readChar(',')); + if (!readChar('}')) { + throw expected("',' or '}'"); + } + nestingLevel--; + handler.endObject(object); + } - private String readStringInternal() throws IOException { - read(); + private String readName() throws IOException { + if (current != '"') { + throw expected("name"); + } + return readStringInternal(); + } + + private void readNull() throws IOException { + handler.startNull(); + read(); + readRequiredChar('u'); + readRequiredChar('l'); + readRequiredChar('l'); + handler.endNull(); + } + + private void readTrue() throws IOException { + handler.startBoolean(); + read(); + readRequiredChar('r'); + readRequiredChar('u'); + readRequiredChar('e'); + handler.endBoolean(true); + } + + private void readFalse() throws IOException { + handler.startBoolean(); + read(); + readRequiredChar('a'); + readRequiredChar('l'); + readRequiredChar('s'); + readRequiredChar('e'); + handler.endBoolean(false); + } + + private void readRequiredChar(char ch) throws IOException { + if (!readChar(ch)) { + throw expected("'" + ch + "'"); + } + } + + private void readString() throws IOException { + handler.startString(); + handler.endString(readStringInternal()); + } + + private String readStringInternal() throws IOException { + read(); + startCapture(); + while (current != '"') { + if (current == '\\') { + pauseCapture(); + readEscape(); startCapture(); - while (current != '"') { - if (current == '\\') { - pauseCapture(); - readEscape(); - startCapture(); - } else if (current < 0x20) { - throw expected("valid string character"); - } else { - read(); - } - } - String string = endCapture(); + } else if (current < 0x20) { + throw expected("valid string character"); + } else { read(); - return string; + } } - - private void readEscape() throws IOException { - read(); - switch (current) { - case '"': - case '/': - case '\\': - captureBuffer.append((char) current); - break; - case 'b': - captureBuffer.append('\b'); - break; - case 'f': - captureBuffer.append('\f'); - break; - case 'n': - captureBuffer.append('\n'); - break; - case 'r': - captureBuffer.append('\r'); - break; - case 't': - captureBuffer.append('\t'); - break; - case 'u': - char[] hexChars = new char[4]; - for (int i = 0; i < 4; i++) { - read(); - if (!isHexDigit()) { - throw expected("hexadecimal digit"); - } - hexChars[i] = (char) current; - } - captureBuffer.append((char) Integer.parseInt(new String(hexChars), 16)); - break; - default: - throw expected("valid escape sequence"); + String string = endCapture(); + read(); + return string; + } + + private void readEscape() throws IOException { + read(); + switch (current) { + case '"': + case '/': + case '\\': + captureBuffer.append((char)current); + break; + case 'b': + captureBuffer.append('\b'); + break; + case 'f': + captureBuffer.append('\f'); + break; + case 'n': + captureBuffer.append('\n'); + break; + case 'r': + captureBuffer.append('\r'); + break; + case 't': + captureBuffer.append('\t'); + break; + case 'u': + char[] hexChars = new char[4]; + for (int i = 0; i < 4; i++) { + read(); + if (!isHexDigit()) { + throw expected("hexadecimal digit"); + } + hexChars[i] = (char)current; } - read(); + captureBuffer.append((char)Integer.parseInt(new String(hexChars), 16)); + break; + default: + throw expected("valid escape sequence"); } - - private JsonValue readNumber() throws IOException { - startCapture(); - readChar('-'); - int firstDigit = current; - if (!readDigit()) { - throw expected("digit"); - } - if (firstDigit != '0') { - while (readDigit()) { - } - } - readFraction(); - readExponent(); - return new JsonNumber(endCapture()); + read(); + } + + private void readNumber() throws IOException { + handler.startNumber(); + startCapture(); + readChar('-'); + int firstDigit = current; + if (!readDigit()) { + throw expected("digit"); } - - private boolean readFraction() throws IOException { - if (!readChar('.')) { - return false; - } - if (!readDigit()) { - throw expected("digit"); - } - while (readDigit()) { - } - return true; + if (firstDigit != '0') { + while (readDigit()) { + } } - - private boolean readExponent() throws IOException { - if (!readChar('e') && !readChar('E')) { - return false; - } - if (!readChar('+')) { - readChar('-'); - } - if (!readDigit()) { - throw expected("digit"); - } - while (readDigit()) { - } - return true; + readFraction(); + readExponent(); + handler.endNumber(endCapture()); + } + + private boolean readFraction() throws IOException { + if (!readChar('.')) { + return false; } - - private boolean readChar(char ch) throws IOException { - if (current != ch) { - return false; - } - read(); - return true; + if (!readDigit()) { + throw expected("digit"); } - - private boolean readDigit() throws IOException { - if (!isDigit()) { - return false; - } - read(); - return true; + while (readDigit()) { } + return true; + } - private void skipWhiteSpace() throws IOException { - while (isWhiteSpace()) { - read(); - } + private boolean readExponent() throws IOException { + if (!readChar('e') && !readChar('E')) { + return false; } - - private void read() throws IOException { - if (index == fill) { - if (captureStart != -1) { - captureBuffer.append(buffer, captureStart, fill - captureStart); - captureStart = 0; - } - bufferOffset += fill; - fill = reader.read(buffer, 0, buffer.length); - index = 0; - if (fill == -1) { - current = -1; - return; - } - } - if (current == '\n') { - line++; - lineOffset = bufferOffset + index; - } - current = buffer[index++]; + if (!readChar('+')) { + readChar('-'); } - - private void startCapture() { - if (captureBuffer == null) { - captureBuffer = new StringBuilder(); - } - captureStart = index - 1; + if (!readDigit()) { + throw expected("digit"); } - - private void pauseCapture() { - int end = current == -1 ? index : index - 1; - captureBuffer.append(buffer, captureStart, end - captureStart); - captureStart = -1; + while (readDigit()) { } + return true; + } - private String endCapture() { - int end = current == -1 ? index : index - 1; - String captured; - if (captureBuffer.length() > 0) { - captureBuffer.append(buffer, captureStart, end - captureStart); - captured = captureBuffer.toString(); - captureBuffer.setLength(0); - } else { - captured = new String(buffer, captureStart, end - captureStart); - } - captureStart = -1; - return captured; + private boolean readChar(char ch) throws IOException { + if (current != ch) { + return false; } + read(); + return true; + } - private ParseException expected(String expected) { - if (isEndOfText()) { - return error("Unexpected end of input"); - } - return error("Expected " + expected); + private boolean readDigit() throws IOException { + if (!isDigit()) { + return false; } + read(); + return true; + } - private ParseException error(String message) { - int absIndex = bufferOffset + index; - int column = absIndex - lineOffset; - int offset = isEndOfText() ? absIndex : absIndex - 1; - return new ParseException(message, offset, line, column - 1); + private void skipWhiteSpace() throws IOException { + while (isWhiteSpace()) { + read(); } - - private boolean isWhiteSpace() { - return current == ' ' || current == '\t' || current == '\n' || current == '\r'; + } + + private void read() throws IOException { + if (index == fill) { + if (captureStart != -1) { + captureBuffer.append(buffer, captureStart, fill - captureStart); + captureStart = 0; + } + bufferOffset += fill; + fill = reader.read(buffer, 0, buffer.length); + index = 0; + if (fill == -1) { + current = -1; + index++; + return; + } } - - private boolean isDigit() { - return current >= '0' && current <= '9'; + if (current == '\n') { + line++; + lineOffset = bufferOffset + index; } + current = buffer[index++]; + } - private boolean isHexDigit() { - return current >= '0' && current <= '9' - || current >= 'a' && current <= 'f' - || current >= 'A' && current <= 'F'; + private void startCapture() { + if (captureBuffer == null) { + captureBuffer = new StringBuilder(); } - - private boolean isEndOfText() { - return current == -1; + captureStart = index - 1; + } + + private void pauseCapture() { + int end = current == -1 ? index : index - 1; + captureBuffer.append(buffer, captureStart, end - captureStart); + captureStart = -1; + } + + private String endCapture() { + int start = captureStart; + int end = index - 1; + captureStart = -1; + if (captureBuffer.length() > 0) { + captureBuffer.append(buffer, start, end - start); + String captured = captureBuffer.toString(); + captureBuffer.setLength(0); + return captured; + } + return new String(buffer, start, end - start); + } + + Location getLocation() { + int offset = bufferOffset + index - 1; + int column = offset - lineOffset + 1; + return new Location(offset, line, column); + } + + private ParseException expected(String expected) { + if (isEndOfText()) { + return error("Unexpected end of input"); } + return error("Expected " + expected); + } + + private ParseException error(String message) { + return new ParseException(message, getLocation()); + } + + private boolean isWhiteSpace() { + return current == ' ' || current == '\t' || current == '\n' || current == '\r'; + } + + private boolean isDigit() { + return current >= '0' && current <= '9'; + } + + private boolean isHexDigit() { + return current >= '0' && current <= '9' + || current >= 'a' && current <= 'f' + || current >= 'A' && current <= 'F'; + } + + private boolean isEndOfText() { + return current == -1; + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonString.java b/src/main/java/io/github/jopenlibs/vault/json/JsonString.java index d3b3a2d7..59375157 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonString.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonString.java @@ -24,51 +24,51 @@ import java.io.IOException; -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID class JsonString extends JsonValue { - private final String string; + private final String string; - JsonString(String string) { - if (string == null) { - throw new NullPointerException("string is null"); - } - this.string = string; + JsonString(String string) { + if (string == null) { + throw new NullPointerException("string is null"); } + this.string = string; + } - @Override - void write(JsonWriter writer) throws IOException { - writer.writeString(string); - } + @Override + void write(JsonWriter writer) throws IOException { + writer.writeString(string); + } - @Override - public boolean isString() { - return true; - } + @Override + public boolean isString() { + return true; + } - @Override - public String asString() { - return string; - } + @Override + public String asString() { + return string; + } - @Override - public int hashCode() { - return string.hashCode(); - } + @Override + public int hashCode() { + return string.hashCode(); + } - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object == null) { - return false; - } - if (getClass() != object.getClass()) { - return false; - } - JsonString other = (JsonString) object; - return string.equals(other.string); + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; } + JsonString other = (JsonString)object; + return string.equals(other.string); + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonValue.java b/src/main/java/io/github/jopenlibs/vault/json/JsonValue.java index 562f4c68..05b5907d 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonValue.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonValue.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2015 EclipseSource. + * Copyright (c) 2013, 2017 EclipseSource. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ package io.github.jopenlibs.vault.json; import java.io.IOException; +import java.io.Reader; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; @@ -32,11 +33,20 @@ * a number, a string, or one of the literals * true, false, and null. *

+ * The literals true, false, and null are + * represented by the constants {@link Json#TRUE}, {@link Json#FALSE}, and {@link Json#NULL}. + *

+ *

* JSON objects and arrays are represented by the subtypes * {@link JsonObject} and {@link JsonArray}. Instances of these types can be created using the * public constructors of these classes. *

*

+ * Instances that represent JSON numbers, strings and + * boolean values can be created using the static factory methods + * {@link Json#value(String)}, {@link Json#value(long)}, {@link Json#value(double)}, etc. + *

+ *

* In order to find out whether an instance of this class is of a certain type, the methods * {@link #isObject()}, {@link #isArray()}, {@link #isString()}, {@link #isNumber()} etc. can be * used. @@ -50,290 +60,436 @@ * This class is not supposed to be extended by clients. *

*/ -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID public abstract class JsonValue implements Serializable { - JsonValue() { - // prevent subclasses outside of this package - } + /** + * Represents the JSON literal true. + * @deprecated Use Json.TRUE instead + */ + @Deprecated + public static final JsonValue TRUE = new JsonLiteral("true"); - /** - * Detects whether this value represents a JSON object. If this is the case, this value is an - * instance of {@link JsonObject}. - * - * @return true if this value is an instance of JsonObject - */ - public boolean isObject() { - return false; - } + /** + * Represents the JSON literal false. + * @deprecated Use Json.FALSE instead + */ + @Deprecated + public static final JsonValue FALSE = new JsonLiteral("false"); - /** - * Detects whether this value represents a JSON array. If this is the case, this value is an - * instance of {@link JsonArray}. - * - * @return true if this value is an instance of JsonArray - */ - public boolean isArray() { - return false; - } + /** + * Represents the JSON literal null. + * @deprecated Use Json.NULL instead + */ + @Deprecated + public static final JsonValue NULL = new JsonLiteral("null"); - /** - * Detects whether this value represents a JSON number. - * - * @return true if this value represents a JSON number - */ - public boolean isNumber() { - return false; - } + JsonValue() { + // prevent subclasses outside of this package + } - /** - * Detects whether this value represents a JSON string. - * - * @return true if this value represents a JSON string - */ - public boolean isString() { - return false; - } + /** + * Reads a JSON value from the given reader. + *

+ * Characters are read in chunks and buffered internally, therefore wrapping an existing reader in + * an additional BufferedReader does not improve reading + * performance. + *

+ * + * @param reader + * the reader to read the JSON value from + * @return the JSON value that has been read + * @throws IOException + * if an I/O error occurs in the reader + * @throws ParseException + * if the input is not valid JSON + * @deprecated Use {@link Json#parse(Reader)} instead + */ + @Deprecated + public static JsonValue readFrom(Reader reader) throws IOException { + return Json.parse(reader); + } - /** - * Detects whether this value represents a boolean value. - * - * @return true if this value represents either the JSON literal true - * or - * false - */ - public boolean isBoolean() { - return false; - } + /** + * Reads a JSON value from the given string. + * + * @param text + * the string that contains the JSON value + * @return the JSON value that has been read + * @throws ParseException + * if the input is not valid JSON + * @deprecated Use {@link Json#parse(String)} instead + */ + @Deprecated + public static JsonValue readFrom(String text) { + return Json.parse(text); + } - /** - * Detects whether this value represents the JSON literal true. - * - * @return true if this value represents the JSON literal true - */ - public boolean isTrue() { - return false; - } + /** + * Returns a JsonValue instance that represents the given int value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + * @deprecated Use Json.value() instead + */ + @Deprecated + public static JsonValue valueOf(int value) { + return Json.value(value); + } - /** - * Detects whether this value represents the JSON literal false. - * - * @return true if this value represents the JSON literal false - */ - public boolean isFalse() { - return false; - } + /** + * Returns a JsonValue instance that represents the given long value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + * @deprecated Use Json.value() instead + */ + @Deprecated + public static JsonValue valueOf(long value) { + return Json.value(value); + } - /** - * Detects whether this value represents the JSON literal null. - * - * @return true if this value represents the JSON literal null - */ - public boolean isNull() { - return false; - } + /** + * Returns a JsonValue instance that represents the given float value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + * @deprecated Use Json.value() instead + */ + @Deprecated + public static JsonValue valueOf(float value) { + return Json.value(value); + } - /** - * Returns this JSON value as {@link JsonObject}, assuming that this value represents a JSON - * object. If this is not the case, an exception is thrown. - * - * @return a JSONObject for this value - * @throws UnsupportedOperationException if this value is not a JSON object - */ - public JsonObject asObject() { - throw new UnsupportedOperationException("Not an object: " + toString()); - } + /** + * Returns a JsonValue instance that represents the given double value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + * @deprecated Use Json.value() instead + */ + @Deprecated + public static JsonValue valueOf(double value) { + return Json.value(value); + } - /** - * Returns this JSON value as {@link JsonArray}, assuming that this value represents a JSON - * array. If this is not the case, an exception is thrown. - * - * @return a JSONArray for this value - * @throws UnsupportedOperationException if this value is not a JSON array - */ - public JsonArray asArray() { - throw new UnsupportedOperationException("Not an array: " + toString()); - } + /** + * Returns a JsonValue instance that represents the given string. + * + * @param string + * the string to get a JSON representation for + * @return a JSON value that represents the given string + * @deprecated Use Json.value() instead + */ + @Deprecated + public static JsonValue valueOf(String string) { + return Json.value(string); + } - /** - * Returns this JSON value as an int value, assuming that this value represents a - * JSON number that can be interpreted as Java int. If this is not the case, an - * exception is thrown. - *

- * To be interpreted as Java int, the JSON number must neither contain an exponent - * nor a fraction part. Moreover, the number must be in the Integer range. - *

- * - * @return this value as int - * @throws UnsupportedOperationException if this value is not a JSON number - * @throws NumberFormatException if this JSON number can not be interpreted as int - * value - */ - public int asInt() { - throw new UnsupportedOperationException("Not a number: " + toString()); - } + /** + * Returns a JsonValue instance that represents the given boolean value. + * + * @param value + * the value to get a JSON representation for + * @return a JSON value that represents the given value + * @deprecated Use Json.value() instead + */ + @Deprecated + public static JsonValue valueOf(boolean value) { + return Json.value(value); + } - /** - * Returns this JSON value as a long value, assuming that this value represents a - * JSON number that can be interpreted as Java long. If this is not the case, an - * exception is thrown. - *

- * To be interpreted as Java long, the JSON number must neither contain an exponent - * nor a fraction part. Moreover, the number must be in the Long range. - *

- * - * @return this value as long - * @throws UnsupportedOperationException if this value is not a JSON number - * @throws NumberFormatException if this JSON number can not be interpreted as long - * value - */ - public long asLong() { - throw new UnsupportedOperationException("Not a number: " + toString()); - } + /** + * Detects whether this value represents a JSON object. If this is the case, this value is an + * instance of {@link JsonObject}. + * + * @return true if this value is an instance of JsonObject + */ + public boolean isObject() { + return false; + } - /** - * Returns this JSON value as a float value, assuming that this value represents a - * JSON number. If this is not the case, an exception is thrown. - *

- * If the JSON number is out of the Float range, {@link Float#POSITIVE_INFINITY} or - * {@link Float#NEGATIVE_INFINITY} is returned. - *

- * - * @return this value as float - * @throws UnsupportedOperationException if this value is not a JSON number - */ - public float asFloat() { - throw new UnsupportedOperationException("Not a number: " + toString()); - } + /** + * Detects whether this value represents a JSON array. If this is the case, this value is an + * instance of {@link JsonArray}. + * + * @return true if this value is an instance of JsonArray + */ + public boolean isArray() { + return false; + } - /** - * Returns this JSON value as a double value, assuming that this value represents a - * JSON number. If this is not the case, an exception is thrown. - *

- * If the JSON number is out of the Double range, {@link Double#POSITIVE_INFINITY} - * or {@link Double#NEGATIVE_INFINITY} is returned. - *

- * - * @return this value as double - * @throws UnsupportedOperationException if this value is not a JSON number - */ - public double asDouble() { - throw new UnsupportedOperationException("Not a number: " + toString()); - } + /** + * Detects whether this value represents a JSON number. + * + * @return true if this value represents a JSON number + */ + public boolean isNumber() { + return false; + } - /** - * Returns this JSON value as String, assuming that this value represents a JSON string. If this - * is not the case, an exception is thrown. - * - * @return the string represented by this value - * @throws UnsupportedOperationException if this value is not a JSON string - */ - public String asString() { - throw new UnsupportedOperationException("Not a string: " + toString()); - } + /** + * Detects whether this value represents a JSON string. + * + * @return true if this value represents a JSON string + */ + public boolean isString() { + return false; + } - /** - * Returns this JSON value as a boolean value, assuming that this value is either - * true or false. If this is not the case, an exception is thrown. - * - * @return this value as boolean - * @throws UnsupportedOperationException if this value is neither true or - * false - */ - public boolean asBoolean() { - throw new UnsupportedOperationException("Not a boolean: " + toString()); - } + /** + * Detects whether this value represents a boolean value. + * + * @return true if this value represents either the JSON literal true or + * false + */ + public boolean isBoolean() { + return false; + } - /** - * Writes the JSON representation of this value to the given writer in its minimal form, without - * any additional whitespace. - *

- * Writing performance can be improved by using a - * {@link java.io.BufferedWriter BufferedWriter}. - *

- * - * @param writer the writer to write this value to - * @throws IOException if an I/O error occurs in the writer - */ - public void writeTo(Writer writer) throws IOException { - writeTo(writer, WriterConfig.MINIMAL); - } + /** + * Detects whether this value represents the JSON literal true. + * + * @return true if this value represents the JSON literal true + */ + public boolean isTrue() { + return false; + } - /** - * Writes the JSON representation of this value to the given writer using the given formatting. - *

- * Writing performance can be improved by using a - * {@link java.io.BufferedWriter BufferedWriter}. - *

- * - * @param writer the writer to write this value to - * @param config a configuration that controls the formatting or null for the - * minimal form - * @throws IOException if an I/O error occurs in the writer - */ - public void writeTo(Writer writer, WriterConfig config) throws IOException { - if (writer == null) { - throw new NullPointerException("writer is null"); - } - if (config == null) { - throw new NullPointerException("config is null"); - } - WritingBuffer buffer = new WritingBuffer(writer, 128); - write(config.createWriter(buffer)); - buffer.flush(); - } + /** + * Detects whether this value represents the JSON literal false. + * + * @return true if this value represents the JSON literal false + */ + public boolean isFalse() { + return false; + } - /** - * Returns the JSON string for this value in its minimal form, without any additional - * whitespace. - * - * @return a JSON string that represents this value - */ - @Override - public String toString() { - return toString(WriterConfig.MINIMAL); - } + /** + * Detects whether this value represents the JSON literal null. + * + * @return true if this value represents the JSON literal null + */ + public boolean isNull() { + return false; + } - /** - * Returns the JSON string for this value using the given formatting. - * - * @param config a configuration that controls the formatting or null for the - * minimal form - * @return a JSON string that represents this value - */ - public String toString(WriterConfig config) { - StringWriter writer = new StringWriter(); - try { - writeTo(writer, config); - } catch (IOException exception) { - // StringWriter does not throw IOExceptions - throw new RuntimeException(exception); - } - return writer.toString(); - } + /** + * Returns this JSON value as {@link JsonObject}, assuming that this value represents a JSON + * object. If this is not the case, an exception is thrown. + * + * @return a JSONObject for this value + * @throws UnsupportedOperationException + * if this value is not a JSON object + */ + public JsonObject asObject() { + throw new UnsupportedOperationException("Not an object: " + toString()); + } + + /** + * Returns this JSON value as {@link JsonArray}, assuming that this value represents a JSON array. + * If this is not the case, an exception is thrown. + * + * @return a JSONArray for this value + * @throws UnsupportedOperationException + * if this value is not a JSON array + */ + public JsonArray asArray() { + throw new UnsupportedOperationException("Not an array: " + toString()); + } + + /** + * Returns this JSON value as an int value, assuming that this value represents a + * JSON number that can be interpreted as Java int. If this is not the case, an + * exception is thrown. + *

+ * To be interpreted as Java int, the JSON number must neither contain an exponent + * nor a fraction part. Moreover, the number must be in the Integer range. + *

+ * + * @return this value as int + * @throws UnsupportedOperationException + * if this value is not a JSON number + * @throws NumberFormatException + * if this JSON number can not be interpreted as int value + */ + public int asInt() { + throw new UnsupportedOperationException("Not a number: " + toString()); + } + + /** + * Returns this JSON value as a long value, assuming that this value represents a + * JSON number that can be interpreted as Java long. If this is not the case, an + * exception is thrown. + *

+ * To be interpreted as Java long, the JSON number must neither contain an exponent + * nor a fraction part. Moreover, the number must be in the Long range. + *

+ * + * @return this value as long + * @throws UnsupportedOperationException + * if this value is not a JSON number + * @throws NumberFormatException + * if this JSON number can not be interpreted as long value + */ + public long asLong() { + throw new UnsupportedOperationException("Not a number: " + toString()); + } - /** - * Indicates whether some other object is "equal to" this one according to the contract - * specified in {@link Object#equals(Object)}. - *

- * Two JsonValues are considered equal if and only if they represent the same JSON text. As a - * consequence, two given JsonObjects may be different even though they contain the same set of - * names with the same values, but in a different order. - *

- * - * @param object the reference object with which to compare - * @return true if this object is the same as the object argument; false otherwise - */ - @Override - public boolean equals(Object object) { - return super.equals(object); + /** + * Returns this JSON value as a float value, assuming that this value represents a + * JSON number. If this is not the case, an exception is thrown. + *

+ * If the JSON number is out of the Float range, {@link Float#POSITIVE_INFINITY} or + * {@link Float#NEGATIVE_INFINITY} is returned. + *

+ * + * @return this value as float + * @throws UnsupportedOperationException + * if this value is not a JSON number + */ + public float asFloat() { + throw new UnsupportedOperationException("Not a number: " + toString()); + } + + /** + * Returns this JSON value as a double value, assuming that this value represents a + * JSON number. If this is not the case, an exception is thrown. + *

+ * If the JSON number is out of the Double range, {@link Double#POSITIVE_INFINITY} or + * {@link Double#NEGATIVE_INFINITY} is returned. + *

+ * + * @return this value as double + * @throws UnsupportedOperationException + * if this value is not a JSON number + */ + public double asDouble() { + throw new UnsupportedOperationException("Not a number: " + toString()); + } + + /** + * Returns this JSON value as String, assuming that this value represents a JSON string. If this + * is not the case, an exception is thrown. + * + * @return the string represented by this value + * @throws UnsupportedOperationException + * if this value is not a JSON string + */ + public String asString() { + throw new UnsupportedOperationException("Not a string: " + toString()); + } + + /** + * Returns this JSON value as a boolean value, assuming that this value is either + * true or false. If this is not the case, an exception is thrown. + * + * @return this value as boolean + * @throws UnsupportedOperationException + * if this value is neither true or false + */ + public boolean asBoolean() { + throw new UnsupportedOperationException("Not a boolean: " + toString()); + } + + /** + * Writes the JSON representation of this value to the given writer in its minimal form, without + * any additional whitespace. + *

+ * Writing performance can be improved by using a {@link java.io.BufferedWriter BufferedWriter}. + *

+ * + * @param writer + * the writer to write this value to + * @throws IOException + * if an I/O error occurs in the writer + */ + public void writeTo(Writer writer) throws IOException { + writeTo(writer, WriterConfig.MINIMAL); + } + + /** + * Writes the JSON representation of this value to the given writer using the given formatting. + *

+ * Writing performance can be improved by using a {@link java.io.BufferedWriter BufferedWriter}. + *

+ * + * @param writer + * the writer to write this value to + * @param config + * a configuration that controls the formatting or null for the minimal form + * @throws IOException + * if an I/O error occurs in the writer + */ + public void writeTo(Writer writer, WriterConfig config) throws IOException { + if (writer == null) { + throw new NullPointerException("writer is null"); } + if (config == null) { + throw new NullPointerException("config is null"); + } + WritingBuffer buffer = new WritingBuffer(writer, 128); + write(config.createWriter(buffer)); + buffer.flush(); + } + + /** + * Returns the JSON string for this value in its minimal form, without any additional whitespace. + * The result is guaranteed to be a valid input for the method {@link Json#parse(String)} and to + * create a value that is equal to this object. + * + * @return a JSON string that represents this value + */ + @Override + public String toString() { + return toString(WriterConfig.MINIMAL); + } - @Override - public int hashCode() { - return super.hashCode(); + /** + * Returns the JSON string for this value using the given formatting. + * + * @param config + * a configuration that controls the formatting or null for the minimal form + * @return a JSON string that represents this value + */ + public String toString(WriterConfig config) { + StringWriter writer = new StringWriter(); + try { + writeTo(writer, config); + } catch (IOException exception) { + // StringWriter does not throw IOExceptions + throw new RuntimeException(exception); } + return writer.toString(); + } + + /** + * Indicates whether some other object is "equal to" this one according to the contract specified + * in {@link Object#equals(Object)}. + *

+ * Two JsonValues are considered equal if and only if they represent the same JSON text. As a + * consequence, two given JsonObjects may be different even though they contain the same set of + * names with the same values, but in a different order. + *

+ * + * @param object + * the reference object with which to compare + * @return true if this object is the same as the object argument; false otherwise + */ + @Override + public boolean equals(Object object) { + return super.equals(object); + } + + @Override + public int hashCode() { + return super.hashCode(); + } - abstract void write(JsonWriter writer) throws IOException; + abstract void write(JsonWriter writer) throws IOException; } diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonWriter.java b/src/main/java/io/github/jopenlibs/vault/json/JsonWriter.java index 3724f13c..933338dd 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonWriter.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonWriter.java @@ -25,123 +25,121 @@ import java.io.Writer; -@SuppressWarnings("PMD") class JsonWriter { - private static final int CONTROL_CHARACTERS_END = 0x001f; - - private static final char[] QUOT_CHARS = {'\\', '"'}; - private static final char[] BS_CHARS = {'\\', '\\'}; - private static final char[] LF_CHARS = {'\\', 'n'}; - private static final char[] CR_CHARS = {'\\', 'r'}; - private static final char[] TAB_CHARS = {'\\', 't'}; - // In JavaScript, U+2028 and U+2029 characters count as line endings and must be encoded. - // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character - private static final char[] UNICODE_2028_CHARS = {'\\', 'u', '2', '0', '2', '8'}; - private static final char[] UNICODE_2029_CHARS = {'\\', 'u', '2', '0', '2', '9'}; - private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f'}; - - protected final Writer writer; - - JsonWriter(Writer writer) { - this.writer = writer; + private static final int CONTROL_CHARACTERS_END = 0x001f; + + private static final char[] QUOT_CHARS = {'\\', '"'}; + private static final char[] BS_CHARS = {'\\', '\\'}; + private static final char[] LF_CHARS = {'\\', 'n'}; + private static final char[] CR_CHARS = {'\\', 'r'}; + private static final char[] TAB_CHARS = {'\\', 't'}; + // In JavaScript, U+2028 and U+2029 characters count as line endings and must be encoded. + // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character + private static final char[] UNICODE_2028_CHARS = {'\\', 'u', '2', '0', '2', '8'}; + private static final char[] UNICODE_2029_CHARS = {'\\', 'u', '2', '0', '2', '9'}; + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f'}; + + protected final Writer writer; + + JsonWriter(Writer writer) { + this.writer = writer; + } + + protected void writeLiteral(String value) throws IOException { + writer.write(value); + } + + protected void writeNumber(String string) throws IOException { + writer.write(string); + } + + protected void writeString(String string) throws IOException { + writer.write('"'); + writeJsonString(string); + writer.write('"'); + } + + protected void writeArrayOpen() throws IOException { + writer.write('['); + } + + protected void writeArrayClose() throws IOException { + writer.write(']'); + } + + protected void writeArraySeparator() throws IOException { + writer.write(','); + } + + protected void writeObjectOpen() throws IOException { + writer.write('{'); + } + + protected void writeObjectClose() throws IOException { + writer.write('}'); + } + + protected void writeMemberName(String name) throws IOException { + writer.write('"'); + writeJsonString(name); + writer.write('"'); + } + + protected void writeMemberSeparator() throws IOException { + writer.write(':'); + } + + protected void writeObjectSeparator() throws IOException { + writer.write(','); + } + + protected void writeJsonString(String string) throws IOException { + int length = string.length(); + int start = 0; + for (int index = 0; index < length; index++) { + char[] replacement = getReplacementChars(string.charAt(index)); + if (replacement != null) { + writer.write(string, start, index - start); + writer.write(replacement); + start = index + 1; + } } - - protected void writeLiteral(String value) throws IOException { - writer.write(value); + writer.write(string, start, length - start); + } + + private static char[] getReplacementChars(char ch) { + if (ch > '\\') { + if (ch < '\u2028' || ch > '\u2029') { + // The lower range contains 'a' .. 'z'. Only 2 checks required. + return null; + } + return ch == '\u2028' ? UNICODE_2028_CHARS : UNICODE_2029_CHARS; } - - protected void writeNumber(String string) throws IOException { - writer.write(string); + if (ch == '\\') { + return BS_CHARS; } - - protected void writeString(String string) throws IOException { - writer.write('"'); - writeJsonString(string); - writer.write('"'); + if (ch > '"') { + // This range contains '0' .. '9' and 'A' .. 'Z'. Need 3 checks to get here. + return null; } - - protected void writeArrayOpen() throws IOException { - writer.write('['); + if (ch == '"') { + return QUOT_CHARS; } - - protected void writeArrayClose() throws IOException { - writer.write(']'); + if (ch > CONTROL_CHARACTERS_END) { + return null; } - - protected void writeArraySeparator() throws IOException { - writer.write(','); + if (ch == '\n') { + return LF_CHARS; } - - protected void writeObjectOpen() throws IOException { - writer.write('{'); + if (ch == '\r') { + return CR_CHARS; } - - protected void writeObjectClose() throws IOException { - writer.write('}'); - } - - protected void writeMemberName(String name) throws IOException { - writer.write('"'); - writeJsonString(name); - writer.write('"'); - } - - protected void writeMemberSeparator() throws IOException { - writer.write(':'); - } - - protected void writeObjectSeparator() throws IOException { - writer.write(','); - } - - protected void writeJsonString(String string) throws IOException { - int length = string.length(); - int start = 0; - for (int index = 0; index < length; index++) { - char[] replacement = getReplacementChars(string.charAt(index)); - if (replacement != null) { - writer.write(string, start, index - start); - writer.write(replacement); - start = index + 1; - } - } - writer.write(string, start, length - start); - } - - private static char[] getReplacementChars(char ch) { - if (ch > '\\') { - if (ch < '\u2028' || ch > '\u2029') { - // The lower range contains 'a' .. 'z'. Only 2 checks required. - return null; - } - return ch == '\u2028' ? UNICODE_2028_CHARS : UNICODE_2029_CHARS; - } - if (ch == '\\') { - return BS_CHARS; - } - if (ch > '"') { - // This range contains '0' .. '9' and 'A' .. 'Z'. Need 3 checks to get here. - return null; - } - if (ch == '"') { - return QUOT_CHARS; - } - if (ch > CONTROL_CHARACTERS_END) { - return null; - } - if (ch == '\n') { - return LF_CHARS; - } - if (ch == '\r') { - return CR_CHARS; - } - if (ch == '\t') { - return TAB_CHARS; - } - return new char[]{'\\', 'u', '0', '0', HEX_DIGITS[ch >> 4 & 0x000f], - HEX_DIGITS[ch & 0x000f]}; + if (ch == '\t') { + return TAB_CHARS; } + return new char[] {'\\', 'u', '0', '0', HEX_DIGITS[ch >> 4 & 0x000f], HEX_DIGITS[ch & 0x000f]}; + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/Location.java b/src/main/java/io/github/jopenlibs/vault/json/Location.java new file mode 100644 index 00000000..0af1f4b4 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/json/Location.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2016 EclipseSource. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ +package io.github.jopenlibs.vault.json; + + +/** + * An immutable object that represents a location in the parsed text. + */ +public class Location { + + /** + * The absolute character index, starting at 0. + */ + public final int offset; + + /** + * The line number, starting at 1. + */ + public final int line; + + /** + * The column number, starting at 1. + */ + public final int column; + + Location(int offset, int line, int column) { + this.offset = offset; + this.column = column; + this.line = line; + } + + @Override + public String toString() { + return line + ":" + column; + } + + @Override + public int hashCode() { + return offset; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Location other = (Location)obj; + return offset == other.offset && column == other.column && line == other.line; + } + +} diff --git a/src/main/java/io/github/jopenlibs/vault/json/ParseException.java b/src/main/java/io/github/jopenlibs/vault/json/ParseException.java index dc0c20d7..6968afa4 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/ParseException.java +++ b/src/main/java/io/github/jopenlibs/vault/json/ParseException.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2015 EclipseSource. + * Copyright (c) 2013, 2016 EclipseSource. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,47 +24,58 @@ /** * An unchecked exception to indicate that an input does not qualify as valid JSON. */ -@SuppressWarnings({"serial", "PMD"}) // use default serial UID +@SuppressWarnings("serial") // use default serial UID public class ParseException extends RuntimeException { - private final int offset; - private final int line; - private final int column; + private final Location location; - ParseException(String message, int offset, int line, int column) { - super(message + " at " + line + ":" + column); - this.offset = offset; - this.line = line; - this.column = column; - } + ParseException(String message, Location location) { + super(message + " at " + location); + this.location = location; + } - /** - * Returns the absolute index of the character at which the error occurred. The index of the - * first character of a document is 0. - * - * @return the character offset at which the error occurred, will be >= 0 - */ - public int getOffset() { - return offset; - } + /** + * Returns the location at which the error occurred. + * + * @return the error location + */ + public Location getLocation() { + return location; + } - /** - * Returns the number of the line in which the error occurred. The first line counts as 1. - * - * @return the line in which the error occurred, will be >= 1 - */ - public int getLine() { - return line; - } + /** + * Returns the absolute character index at which the error occurred. The offset of the first + * character of a document is 0. + * + * @return the character offset at which the error occurred, will be >= 0 + * @deprecated Use {@link #getLocation()} instead + */ + @Deprecated + public int getOffset() { + return location.offset; + } - /** - * Returns the index of the character at which the error occurred, relative to the line. The - * index of the first character of a line is 0. - * - * @return the column in which the error occurred, will be >= 0 - */ - public int getColumn() { - return column; - } + /** + * Returns the line number in which the error occurred. The number of the first line is 1. + * + * @return the line in which the error occurred, will be >= 1 + * @deprecated Use {@link #getLocation()} instead + */ + @Deprecated + public int getLine() { + return location.line; + } + + /** + * Returns the column number at which the error occurred, i.e. the number of the character in its + * line. The number of the first character of a line is 1. + * + * @return the column in which the error occurred, will be >= 1 + * @deprecated Use {@link #getLocation()} instead + */ + @Deprecated + public int getColumn() { + return location.column; + } } diff --git a/src/main/java/io/github/jopenlibs/vault/json/PrettyPrint.java b/src/main/java/io/github/jopenlibs/vault/json/PrettyPrint.java index 849e3c7a..439d6970 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/PrettyPrint.java +++ b/src/main/java/io/github/jopenlibs/vault/json/PrettyPrint.java @@ -34,124 +34,124 @@ * jsonValue.writeTo(writer, PrettyPrint.singleLine()); * */ -@SuppressWarnings("PMD") public class PrettyPrint extends WriterConfig { + private final char[] indentChars; + + protected PrettyPrint(char[] indentChars) { + this.indentChars = indentChars; + } + + /** + * Print every value on a separate line. Use tabs (\t) for indentation. + * + * @return A PrettyPrint instance for wrapped mode with tab indentation + */ + public static PrettyPrint singleLine() { + return new PrettyPrint(null); + } + + /** + * Print every value on a separate line. Use the given number of spaces for indentation. + * + * @param number + * the number of spaces to use + * @return A PrettyPrint instance for wrapped mode with spaces indentation + */ + public static PrettyPrint indentWithSpaces(int number) { + if (number < 0) { + throw new IllegalArgumentException("number is negative"); + } + char[] chars = new char[number]; + Arrays.fill(chars, ' '); + return new PrettyPrint(chars); + } + + /** + * Do not break lines, but still insert whitespace between values. + * + * @return A PrettyPrint instance for single-line mode + */ + public static PrettyPrint indentWithTabs() { + return new PrettyPrint(new char[] {'\t'}); + } + + @Override + protected JsonWriter createWriter(Writer writer) { + return new PrettyPrintWriter(writer, indentChars); + } + + private static class PrettyPrintWriter extends JsonWriter { + private final char[] indentChars; + private int indent; + + private PrettyPrintWriter(Writer writer, char[] indentChars) { + super(writer); + this.indentChars = indentChars; + } + + @Override + protected void writeArrayOpen() throws IOException { + indent++; + writer.write('['); + writeNewLine(); + } - protected PrettyPrint(char[] indentChars) { - this.indentChars = indentChars; + @Override + protected void writeArrayClose() throws IOException { + indent--; + writeNewLine(); + writer.write(']'); } - /** - * Print every value on a separate line. Use tabs (\t) for indentation. - * - * @return A PrettyPrint instance for wrapped mode with tab indentation - */ - public static PrettyPrint singleLine() { - return new PrettyPrint(null); + @Override + protected void writeArraySeparator() throws IOException { + writer.write(','); + if (!writeNewLine()) { + writer.write(' '); + } } - /** - * Print every value on a separate line. Use the given number of spaces for indentation. - * - * @param number the number of spaces to use - * @return A PrettyPrint instance for wrapped mode with spaces indentation - */ - public static PrettyPrint indentWithSpaces(int number) { - if (number < 0) { - throw new IllegalArgumentException("number is negative"); - } - char[] chars = new char[number]; - Arrays.fill(chars, ' '); - return new PrettyPrint(chars); + @Override + protected void writeObjectOpen() throws IOException { + indent++; + writer.write('{'); + writeNewLine(); } - /** - * Do not break lines, but still insert whitespace between values. - * - * @return A PrettyPrint instance for single-line mode - */ - public static PrettyPrint indentWithTabs() { - return new PrettyPrint(new char[]{'\t'}); + @Override + protected void writeObjectClose() throws IOException { + indent--; + writeNewLine(); + writer.write('}'); } @Override - protected JsonWriter createWriter(Writer writer) { - return new PrettyPrintWriter(writer, indentChars); + protected void writeMemberSeparator() throws IOException { + writer.write(':'); + writer.write(' '); } - private static class PrettyPrintWriter extends JsonWriter { - - private final char[] indentChars; - private int indent; - - private PrettyPrintWriter(Writer writer, char[] indentChars) { - super(writer); - this.indentChars = indentChars; - } - - @Override - protected void writeArrayOpen() throws IOException { - indent++; - writer.write('['); - writeNewLine(); - } - - @Override - protected void writeArrayClose() throws IOException { - indent--; - writeNewLine(); - writer.write(']'); - } - - @Override - protected void writeArraySeparator() throws IOException { - writer.write(','); - if (!writeNewLine()) { - writer.write(' '); - } - } - - @Override - protected void writeObjectOpen() throws IOException { - indent++; - writer.write('{'); - writeNewLine(); - } - - @Override - protected void writeObjectClose() throws IOException { - indent--; - writeNewLine(); - writer.write('}'); - } - - @Override - protected void writeMemberSeparator() throws IOException { - writer.write(':'); - writer.write(' '); - } - - @Override - protected void writeObjectSeparator() throws IOException { - writer.write(','); - if (!writeNewLine()) { - writer.write(' '); - } - } - - private boolean writeNewLine() throws IOException { - if (indentChars == null) { - return false; - } - writer.write('\n'); - for (int i = 0; i < indent; i++) { - writer.write(indentChars); - } - return true; - } + @Override + protected void writeObjectSeparator() throws IOException { + writer.write(','); + if (!writeNewLine()) { + writer.write(' '); + } + } + private boolean writeNewLine() throws IOException { + if (indentChars == null) { + return false; + } + writer.write('\n'); + for (int i = 0; i < indent; i++) { + writer.write(indentChars); + } + return true; } + } + } diff --git a/src/main/java/io/github/jopenlibs/vault/json/WriterConfig.java b/src/main/java/io/github/jopenlibs/vault/json/WriterConfig.java index a690cbe2..bdf08a78 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/WriterConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/json/WriterConfig.java @@ -27,25 +27,24 @@ /** * Controls the formatting of the JSON output. Use one of the available constants. */ -@SuppressWarnings("PMD") public abstract class WriterConfig { - /** - * Write JSON in its minimal form, without any additional whitespace. This is the default. - */ - public static final WriterConfig MINIMAL = new WriterConfig() { - @Override - JsonWriter createWriter(Writer writer) { - return new JsonWriter(writer); - } - }; + /** + * Write JSON in its minimal form, without any additional whitespace. This is the default. + */ + public static WriterConfig MINIMAL = new WriterConfig() { + @Override + JsonWriter createWriter(Writer writer) { + return new JsonWriter(writer); + } + }; - /** - * Write JSON in pretty-print, with each value on a separate line and an indentation of two - * spaces. - */ - public static WriterConfig PRETTY_PRINT = PrettyPrint.indentWithSpaces(2); + /** + * Write JSON in pretty-print, with each value on a separate line and an indentation of two + * spaces. + */ + public static WriterConfig PRETTY_PRINT = PrettyPrint.indentWithSpaces(2); - abstract JsonWriter createWriter(Writer writer); + abstract JsonWriter createWriter(Writer writer); } diff --git a/src/main/java/io/github/jopenlibs/vault/json/WritingBuffer.java b/src/main/java/io/github/jopenlibs/vault/json/WritingBuffer.java index 6e485a84..8af44ceb 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/WritingBuffer.java +++ b/src/main/java/io/github/jopenlibs/vault/json/WritingBuffer.java @@ -31,70 +31,69 @@ * contract of Writer. In particular, it does not flush or close the wrapped writer nor does it * ensure that the wrapped writer is open. */ -@SuppressWarnings("PMD") class WritingBuffer extends Writer { - private final Writer writer; - private final char[] buffer; - private int fill = 0; + private final Writer writer; + private final char[] buffer; + private int fill = 0; - WritingBuffer(Writer writer) { - this(writer, 16); - } + WritingBuffer(Writer writer) { + this(writer, 16); + } - WritingBuffer(Writer writer, int bufferSize) { - this.writer = writer; - buffer = new char[bufferSize]; - } + WritingBuffer(Writer writer, int bufferSize) { + this.writer = writer; + buffer = new char[bufferSize]; + } - @Override - public void write(int c) throws IOException { - if (fill > buffer.length - 1) { - flush(); - } - buffer[fill++] = (char) c; + @Override + public void write(int c) throws IOException { + if (fill > buffer.length - 1) { + flush(); } + buffer[fill++] = (char)c; + } - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - if (fill > buffer.length - len) { - flush(); - if (len > buffer.length) { - writer.write(cbuf, off, len); - return; - } - } - System.arraycopy(cbuf, off, buffer, fill, len); - fill += len; + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if (fill > buffer.length - len) { + flush(); + if (len > buffer.length) { + writer.write(cbuf, off, len); + return; + } } + System.arraycopy(cbuf, off, buffer, fill, len); + fill += len; + } - @Override - public void write(String str, int off, int len) throws IOException { - if (fill > buffer.length - len) { - flush(); - if (len > buffer.length) { - writer.write(str, off, len); - return; - } - } - str.getChars(off, off + len, buffer, fill); - fill += len; + @Override + public void write(String str, int off, int len) throws IOException { + if (fill > buffer.length - len) { + flush(); + if (len > buffer.length) { + writer.write(str, off, len); + return; + } } + str.getChars(off, off + len, buffer, fill); + fill += len; + } - /** - * Flushes the internal buffer but does not flush the wrapped writer. - */ - @Override - public void flush() throws IOException { - writer.write(buffer, 0, fill); - fill = 0; - } + /** + * Flushes the internal buffer but does not flush the wrapped writer. + */ + @Override + public void flush() throws IOException { + writer.write(buffer, 0, fill); + fill = 0; + } - /** - * Does not close or flush the wrapped writer. - */ - @Override - public void close() throws IOException { - } + /** + * Does not close or flush the wrapped writer. + */ + @Override + public void close() throws IOException { + } } diff --git a/src/main/java/io/github/jopenlibs/vault/response/MountResponse.java b/src/main/java/io/github/jopenlibs/vault/response/MountResponse.java index e3657cf3..84025e00 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/MountResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/MountResponse.java @@ -6,6 +6,7 @@ import io.github.jopenlibs.vault.api.sys.mounts.MountType; import io.github.jopenlibs.vault.json.JsonObject; import io.github.jopenlibs.vault.json.JsonObject.Member; +import io.github.jopenlibs.vault.json.JsonValue; import io.github.jopenlibs.vault.rest.RestResponse; import java.util.Collections; import java.util.Map; @@ -48,11 +49,11 @@ private Mount buildMount(JsonObject data) { return null; } - final MountType type = MountType.of(data.getString("type")); - final String description = data.getString("description"); + final MountType type = MountType.of(getString(data, "type")); + final String description = getString(data, "description"); final MountConfig config = buildMountConfig(data); - final Boolean local = data.getBoolean("local"); - final Boolean sealWrap = data.getBoolean("seal_wrap"); + final Boolean local = getBoolean(data, "local"); + final Boolean sealWrap = getBoolean(data, "seal_wrap"); if (type == null && description == null && config == null && local == null && sealWrap == null) { @@ -79,11 +80,11 @@ private MountConfig buildMountConfig(JsonObject data) { return null; } - final Integer defaultLeaseTtl = config.getInt("default_lease_ttl"); - final Integer maxLeaseTtl = config.getInt("max_lease_ttl"); - final String description = config.getString("description"); - final Boolean forceNoCache = config.getBoolean("force_no_cache"); - final String pluginName = config.getString("plugin_name"); + final Integer defaultLeaseTtl = getInt(config, "default_lease_ttl"); + final Integer maxLeaseTtl = getInt(config, "max_lease_ttl"); + final String description = getString(config, "description"); + final Boolean forceNoCache = getBoolean(config, "force_no_cache"); + final String pluginName = getString(config, "plugin_name"); if (defaultLeaseTtl == null && maxLeaseTtl == null && description == null && forceNoCache == null && pluginName == null) { @@ -109,4 +110,19 @@ private Map buildMountsMap() { .collect(Collectors.toMap(Member::getName, member -> buildMount(member.getValue().asObject()))); } + + private static String getString(final JsonObject object, final String name) { + final JsonValue value = object.get(name); + return value != null ? value.asString() : null; + } + + private static Integer getInt(final JsonObject object, final String name) { + final JsonValue value = object.get(name); + return value != null ? value.asInt() : null; + } + + private static Boolean getBoolean(final JsonObject object, final String name) { + final JsonValue value = object.get(name); + return value != null ? value.asBoolean() : null; + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonArray_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonArray_Test.java index afff9d18..3893a8a6 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonArray_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonArray_Test.java @@ -22,6 +22,7 @@ package io.github.jopenlibs.vault.json; import java.io.IOException; +import java.io.StringReader; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; @@ -29,607 +30,642 @@ import org.junit.Test; import org.mockito.InOrder; +import static io.github.jopenlibs.vault.json.TestUtil.assertException; +import static io.github.jopenlibs.vault.json.TestUtil.serializeAndDeserialize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -@SuppressWarnings("PMD") public class JsonArray_Test { - private JsonArray array; + private JsonArray array; + + @Before + public void setUp() { + array = new JsonArray(); + } + + @Test + public void copyConstructor_failsWithNull() { + assertException(NullPointerException.class, "array is null", new Runnable() { + public void run() { + new JsonArray(null); + } + }); + } + + @Test + public void copyConstructor_hasSameValues() { + array.add(23); + JsonArray copy = new JsonArray(array); + + assertEquals(array.values(), copy.values()); + } + + @Test + public void copyConstructor_worksOnSafeCopy() { + JsonArray copy = new JsonArray(array); + array.add(23); + + assertTrue(copy.isEmpty()); + } + + @Test + public void unmodifiableArray_hasSameValues() { + array.add(23); + JsonArray unmodifiableArray = JsonArray.unmodifiableArray(array); + + assertEquals(array.values(), unmodifiableArray.values()); + } + + @Test + public void unmodifiableArray_reflectsChanges() { + JsonArray unmodifiableArray = JsonArray.unmodifiableArray(array); + array.add(23); + + assertEquals(array.values(), unmodifiableArray.values()); + } + + @Test(expected = UnsupportedOperationException.class) + public void unmodifiableArray_preventsModification() { + JsonArray unmodifiableArray = JsonArray.unmodifiableArray(array); + + unmodifiableArray.add(23); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_reader() throws IOException { + assertEquals(new JsonArray(), JsonArray.readFrom(new StringReader("[]"))); + assertEquals(new JsonArray().add("a").add(23), + JsonArray.readFrom(new StringReader("[ \"a\", 23 ]"))); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_string() { + assertEquals(new JsonArray(), JsonArray.readFrom("[]")); + assertEquals(new JsonArray().add("a").add(23), JsonArray.readFrom("[ \"a\", 23 ]")); + } + + @Test(expected = ParseException.class) + @SuppressWarnings("deprecation") + public void readFrom_illegalJson() { + JsonArray.readFrom("This is not JSON"); + } + + @Test(expected = UnsupportedOperationException.class) + @SuppressWarnings("deprecation") + public void readFrom_wrongJsonType() { + JsonArray.readFrom("\"This is not a JSON object\""); + } + + @Test + public void isEmpty_isTrueAfterCreation() { + assertTrue(array.isEmpty()); + } + + @Test + public void isEmpty_isFalseAfterAdd() { + array.add(true); + + assertFalse(array.isEmpty()); + } + + @Test + public void size_isZeroAfterCreation() { + assertEquals(0, array.size()); + } + + @Test + public void size_isOneAfterAdd() { + array.add(true); + + assertEquals(1, array.size()); + } + + @Test + public void iterator_isEmptyAfterCreation() { + assertFalse(array.iterator().hasNext()); + } + + @Test + public void iterator_hasNextAfterAdd() { + array.add(true); + + Iterator iterator = array.iterator(); + assertTrue(iterator.hasNext()); + assertEquals(Json.TRUE, iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test(expected = UnsupportedOperationException.class) + public void iterator_doesNotAllowModification() { + array.add(23); + Iterator iterator = array.iterator(); + iterator.next(); + iterator.remove(); + } + + @Test(expected = ConcurrentModificationException.class) + public void iterator_detectsConcurrentModification() { + Iterator iterator = array.iterator(); + array.add(23); + iterator.next(); + } + + @Test + public void values_isEmptyAfterCreation() { + assertTrue(array.values().isEmpty()); + } + + @Test + public void values_containsValueAfterAdd() { + array.add(true); + + assertEquals(1, array.values().size()); + assertEquals(Json.TRUE, array.values().get(0)); + } + + @Test + public void values_reflectsChanges() { + List values = array.values(); + + array.add(true); + + assertEquals(array.values(), values); + } + + @Test(expected = UnsupportedOperationException.class) + public void values_preventsModification() { + List values = array.values(); + + values.add(Json.TRUE); + } + + @Test + public void get_returnsValue() { + array.add(23); + + JsonValue value = array.get(0); + + assertEquals(Json.value(23), value); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void get_failsWithInvalidIndex() { + array.get(0); + } - @Before - public void setUp() { - array = new JsonArray(); - } - - @Test - public void copyConstructor_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "array is null", - (Runnable) () -> new JsonArray(null)); - } - - @Test - public void copyConstructor_hasSameValues() { - array.add(23); - JsonArray copy = new JsonArray(array); - - assertEquals(array.values(), copy.values()); - } - - @Test - public void copyConstructor_worksOnSafeCopy() { - JsonArray copy = new JsonArray(array); - array.add(23); - - assertTrue(copy.isEmpty()); - } - - @Test - public void unmodifiableArray_hasSameValues() { - array.add(23); - JsonArray unmodifiableArray = JsonArray.unmodifiableArray(array); - - assertEquals(array.values(), unmodifiableArray.values()); - } - - @Test - public void unmodifiableArray_reflectsChanges() { - JsonArray unmodifiableArray = JsonArray.unmodifiableArray(array); - array.add(23); - - assertEquals(array.values(), unmodifiableArray.values()); - } - - @Test(expected = UnsupportedOperationException.class) - public void unmodifiableArray_preventsModification() { - JsonArray unmodifiableArray = JsonArray.unmodifiableArray(array); - - unmodifiableArray.add(23); - } + @Test + public void add_int() { + array.add(23); + + assertEquals("[23]", array.toString()); + } - @Test - public void isEmpty_isTrueAfterCreation() { - assertTrue(array.isEmpty()); - } - - @Test - public void isEmpty_isFalseAfterAdd() { - array.add(true); - - assertFalse(array.isEmpty()); - } - - @Test - public void size_isZeroAfterCreation() { - assertEquals(0, array.size()); - } - - @Test - public void size_isOneAfterAdd() { - array.add(true); - - assertEquals(1, array.size()); - } + @Test + public void add_int_enablesChaining() { + assertSame(array, array.add(23)); + } + + @Test + public void add_long() { + array.add(23l); + + assertEquals("[23]", array.toString()); + } + + @Test + public void add_long_enablesChaining() { + assertSame(array, array.add(23l)); + } - @Test - public void iterator_isEmptyAfterCreation() { - assertFalse(array.iterator().hasNext()); - } + @Test + public void add_float() { + array.add(3.14f); + + assertEquals("[3.14]", array.toString()); + } - @Test - public void iterator_hasNextAfterAdd() { - array.add(true); + @Test + public void add_float_enablesChaining() { + assertSame(array, array.add(3.14f)); + } - Iterator iterator = array.iterator(); - assertTrue(iterator.hasNext()); - assertEquals(Json.TRUE, iterator.next()); - assertFalse(iterator.hasNext()); - } + @Test + public void add_double() { + array.add(3.14d); - @Test(expected = UnsupportedOperationException.class) - public void iterator_doesNotAllowModification() { - array.add(23); - Iterator iterator = array.iterator(); - iterator.next(); - iterator.remove(); - } + assertEquals("[3.14]", array.toString()); + } - @Test(expected = ConcurrentModificationException.class) - public void iterator_detectsConcurrentModification() { - Iterator iterator = array.iterator(); - array.add(23); - iterator.next(); - } + @Test + public void add_double_enablesChaining() { + assertSame(array, array.add(3.14d)); + } - @Test - public void values_isEmptyAfterCreation() { - assertTrue(array.values().isEmpty()); - } + @Test + public void add_boolean() { + array.add(true); - @Test - public void values_containsValueAfterAdd() { - array.add(true); + assertEquals("[true]", array.toString()); + } - assertEquals(1, array.values().size()); - assertEquals(Json.TRUE, array.values().get(0)); - } - - @Test - public void values_reflectsChanges() { - List values = array.values(); + @Test + public void add_boolean_enablesChaining() { + assertSame(array, array.add(true)); + } - array.add(true); - - assertEquals(array.values(), values); - } + @Test + public void add_string() { + array.add("foo"); - @Test(expected = UnsupportedOperationException.class) - public void values_preventsModification() { - List values = array.values(); + assertEquals("[\"foo\"]", array.toString()); + } - values.add(Json.TRUE); - } + @Test + public void add_string_enablesChaining() { + assertSame(array, array.add("foo")); + } - @Test - public void get_returnsValue() { - array.add(23); + @Test + public void add_string_toleratesNull() { + array.add((String)null); - JsonValue value = array.get(0); + assertEquals("[null]", array.toString()); + } - assertEquals(Json.value(23), value); - } + @Test + public void add_jsonNull() { + array.add(Json.NULL); - @Test(expected = IndexOutOfBoundsException.class) - public void get_failsWithInvalidIndex() { - array.get(0); - } + assertEquals("[null]", array.toString()); + } - @Test - public void add_int() { - array.add(23); + @Test + public void add_jsonArray() { + array.add(new JsonArray()); - assertEquals("[23]", array.toString()); - } + assertEquals("[[]]", array.toString()); + } - @Test - public void add_int_enablesChaining() { - assertSame(array, array.add(23)); - } + @Test + public void add_jsonObject() { + array.add(new JsonObject()); - @Test - public void add_long() { - array.add(23L); + assertEquals("[{}]", array.toString()); + } - assertEquals("[23]", array.toString()); - } + @Test + public void add_json_enablesChaining() { + assertSame(array, array.add(Json.NULL)); + } - @Test - public void add_long_enablesChaining() { - assertSame(array, array.add(23L)); - } + @Test + public void add_json_failsWithNull() { + assertException(NullPointerException.class, "value is null", new Runnable() { + public void run() { + array.add((JsonValue)null); + } + }); + } - @Test - public void add_float() { - array.add(3.14f); + @Test + public void add_json_nestedArray() { + JsonArray innerArray = new JsonArray(); + innerArray.add(23); - assertEquals("[3.14]", array.toString()); - } + array.add(innerArray); - @Test - public void add_float_enablesChaining() { - assertSame(array, array.add(3.14f)); - } + assertEquals("[[23]]", array.toString()); + } - @Test - public void add_double() { - array.add(3.14d); + @Test + public void add_json_nestedArray_modifiedAfterAdd() { + JsonArray innerArray = new JsonArray(); + array.add(innerArray); - assertEquals("[3.14]", array.toString()); - } + innerArray.add(23); - @Test - public void add_double_enablesChaining() { - assertSame(array, array.add(3.14d)); - } + assertEquals("[[23]]", array.toString()); + } - @Test - public void add_boolean() { - array.add(true); + @Test + public void add_json_nestedObject() { + JsonObject innerObject = new JsonObject(); + innerObject.add("a", 23); - assertEquals("[true]", array.toString()); - } + array.add(innerObject); - @Test - public void add_boolean_enablesChaining() { - assertSame(array, array.add(true)); - } + assertEquals("[{\"a\":23}]", array.toString()); + } - @Test - public void add_string() { - array.add("foo"); + @Test + public void add_json_nestedObject_modifiedAfterAdd() { + JsonObject innerObject = new JsonObject(); + array.add(innerObject); - assertEquals("[\"foo\"]", array.toString()); - } + innerObject.add("a", 23); - @Test - public void add_string_enablesChaining() { - assertSame(array, array.add("foo")); - } + assertEquals("[{\"a\":23}]", array.toString()); + } - @Test - public void add_string_toleratesNull() { - array.add((String) null); + @Test + public void set_int() { + array.add(false); + array.set(0, 23); - assertEquals("[null]", array.toString()); - } + assertEquals("[23]", array.toString()); + } - @Test - public void add_jsonNull() { - array.add(Json.NULL); + @Test + public void set_int_enablesChaining() { + array.add(false); - assertEquals("[null]", array.toString()); - } + assertSame(array, array.set(0, 23)); + } - @Test - public void add_jsonArray() { - array.add(new JsonArray()); + @Test + public void set_long() { + array.add(false); - assertEquals("[[]]", array.toString()); - } + array.set(0, 23l); - @Test - public void add_jsonObject() { - array.add(new JsonObject()); + assertEquals("[23]", array.toString()); + } - assertEquals("[{}]", array.toString()); - } + @Test + public void set_long_enablesChaining() { + array.add(false); - @Test - public void add_json_enablesChaining() { - assertSame(array, array.add(Json.NULL)); - } + assertSame(array, array.set(0, 23l)); + } - @Test - public void add_json_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "value is null", - (Runnable) () -> array.add((JsonValue) null)); - } + @Test + public void set_float() { + array.add(false); - @Test - public void add_json_nestedArray() { - JsonArray innerArray = new JsonArray(); - innerArray.add(23); + array.set(0, 3.14f); - array.add(innerArray); + assertEquals("[3.14]", array.toString()); + } - assertEquals("[[23]]", array.toString()); - } + @Test + public void set_float_enablesChaining() { + array.add(false); - @Test - public void add_json_nestedArray_modifiedAfterAdd() { - JsonArray innerArray = new JsonArray(); - array.add(innerArray); + assertSame(array, array.set(0, 3.14f)); + } - innerArray.add(23); + @Test + public void set_double() { + array.add(false); - assertEquals("[[23]]", array.toString()); - } + array.set(0, 3.14d); - @Test - public void add_json_nestedObject() { - JsonObject innerObject = new JsonObject(); - innerObject.add("a", 23); + assertEquals("[3.14]", array.toString()); + } - array.add(innerObject); + @Test + public void set_double_enablesChaining() { + array.add(false); - assertEquals("[{\"a\":23}]", array.toString()); - } + assertSame(array, array.set(0, 3.14d)); + } - @Test - public void add_json_nestedObject_modifiedAfterAdd() { - JsonObject innerObject = new JsonObject(); - array.add(innerObject); + @Test + public void set_boolean() { + array.add(false); - innerObject.add("a", 23); + array.set(0, true); - assertEquals("[{\"a\":23}]", array.toString()); - } + assertEquals("[true]", array.toString()); + } - @Test - public void set_int() { - array.add(false); - array.set(0, 23); + @Test + public void set_boolean_enablesChaining() { + array.add(false); - assertEquals("[23]", array.toString()); - } + assertSame(array, array.set(0, true)); + } - @Test - public void set_int_enablesChaining() { - array.add(false); + @Test + public void set_string() { + array.add(false); - assertSame(array, array.set(0, 23)); - } + array.set(0, "foo"); - @Test - public void set_long() { - array.add(false); + assertEquals("[\"foo\"]", array.toString()); + } - array.set(0, 23L); + @Test + public void set_string_enablesChaining() { + array.add(false); - assertEquals("[23]", array.toString()); - } + assertSame(array, array.set(0, "foo")); + } - @Test - public void set_long_enablesChaining() { - array.add(false); + @Test + public void set_jsonNull() { + array.add(false); - assertSame(array, array.set(0, 23L)); - } + array.set(0, Json.NULL); - @Test - public void set_float() { - array.add(false); + assertEquals("[null]", array.toString()); + } - array.set(0, 3.14f); + @Test + public void set_jsonArray() { + array.add(false); - assertEquals("[3.14]", array.toString()); - } + array.set(0, new JsonArray()); - @Test - public void set_float_enablesChaining() { - array.add(false); + assertEquals("[[]]", array.toString()); + } - assertSame(array, array.set(0, 3.14f)); - } + @Test + public void set_jsonObject() { + array.add(false); - @Test - public void set_double() { - array.add(false); + array.set(0, new JsonObject()); - array.set(0, 3.14d); + assertEquals("[{}]", array.toString()); + } - assertEquals("[3.14]", array.toString()); - } + @Test + public void set_json_failsWithNull() { + array.add(false); - @Test - public void set_double_enablesChaining() { - array.add(false); + assertException(NullPointerException.class, "value is null", new Runnable() { + public void run() { + array.set(0, (JsonValue)null); + } + }); + } - assertSame(array, array.set(0, 3.14d)); - } + @Test(expected = IndexOutOfBoundsException.class) + public void set_json_failsWithInvalidIndex() { + array.set(0, Json.NULL); + } - @Test - public void set_boolean() { - array.add(false); + @Test + public void set_json_enablesChaining() { + array.add(false); - array.set(0, true); + assertSame(array, array.set(0, Json.NULL)); + } - assertEquals("[true]", array.toString()); - } + @Test + public void set_json_replacesDifferntArrayElements() { + array.add(3).add(6).add(9); - @Test - public void set_boolean_enablesChaining() { - array.add(false); + array.set(1, 4).set(2, 5); - assertSame(array, array.set(0, true)); - } + assertEquals("[3,4,5]", array.toString()); + } - @Test - public void set_string() { - array.add(false); + @Test(expected = IndexOutOfBoundsException.class) + public void remove_failsWithInvalidIndex() { + array.remove(0); + } - array.set(0, "foo"); + @Test + public void remove_removesElement() { + array.add(23); - assertEquals("[\"foo\"]", array.toString()); - } + array.remove(0); - @Test - public void set_string_enablesChaining() { - array.add(false); + assertEquals("[]", array.toString()); + } + + @Test + public void remove_keepsOtherElements() { + array.add("a").add("b").add("c"); + + array.remove(1); + + assertEquals("[\"a\",\"c\"]", array.toString()); + } + + @Test + public void write_empty() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + array.write(writer); - assertSame(array, array.set(0, "foo")); - } - - @Test - public void set_jsonNull() { - array.add(false); - - array.set(0, Json.NULL); - - assertEquals("[null]", array.toString()); - } - - @Test - public void set_jsonArray() { - array.add(false); - - array.set(0, new JsonArray()); - - assertEquals("[[]]", array.toString()); - } - - @Test - public void set_jsonObject() { - array.add(false); - - array.set(0, new JsonObject()); - - assertEquals("[{}]", array.toString()); - } - - @Test - public void set_json_failsWithNull() { - array.add(false); - - TestUtil.assertException(NullPointerException.class, "value is null", - (Runnable) () -> array.set(0, (JsonValue) null)); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void set_json_failsWithInvalidIndex() { - array.set(0, Json.NULL); - } - - @Test - public void set_json_enablesChaining() { - array.add(false); - - assertSame(array, array.set(0, Json.NULL)); - } - - @Test - public void set_json_replacesDifferntArrayElements() { - array.add(3).add(6).add(9); - - array.set(1, 4).set(2, 5); - - assertEquals("[3,4,5]", array.toString()); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void remove_failsWithInvalidIndex() { - array.remove(0); - } - - @Test - public void remove_removesElement() { - array.add(23); - - array.remove(0); - - assertEquals("[]", array.toString()); - } - - @Test - public void remove_keepsOtherElements() { - array.add("a").add("b").add("c"); - - array.remove(1); - - assertEquals("[\"a\",\"c\"]", array.toString()); - } - - @Test - public void write_empty() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - array.write(writer); - - InOrder inOrder = inOrder(writer); - inOrder.verify(writer).writeArrayOpen(); - inOrder.verify(writer).writeArrayClose(); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void write_withSingleValue() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - array.add(23); - - array.write(writer); - - InOrder inOrder = inOrder(writer); - inOrder.verify(writer).writeArrayOpen(); - inOrder.verify(writer).writeNumber("23"); - inOrder.verify(writer).writeArrayClose(); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void write_withMultipleValues() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - array.add(23).add("foo").add(false); - - array.write(writer); - - InOrder inOrder = inOrder(writer); - inOrder.verify(writer).writeArrayOpen(); - inOrder.verify(writer).writeNumber("23"); - inOrder.verify(writer).writeArraySeparator(); - inOrder.verify(writer).writeString("foo"); - inOrder.verify(writer).writeArraySeparator(); - inOrder.verify(writer).writeLiteral("false"); - inOrder.verify(writer).writeArrayClose(); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void isArray() { - assertTrue(array.isArray()); - } - - @Test - public void asArray() { - assertSame(array, array.asArray()); - } - - @Test - public void equals_trueForSameInstance() { - assertEquals(array, array); - } - - @Test - public void equals_trueForEqualArrays() { - assertEquals(array(), array()); - assertEquals(array("foo", "bar"), array("foo", "bar")); - } - - @Test - public void equals_falseForDifferentArrays() { - assertNotEquals(array("foo", "bar"), array("foo", "bar", "baz")); - assertNotEquals(array("foo", "bar"), array("bar", "foo")); - } - - @Test - public void equals_falseForNull() { - assertNotEquals(null, array); - } - - @Test - public void equals_falseForSubclass() { - assertNotEquals(array, new JsonArray(array) { - }); - } - - @Test - public void hashCode_equalsForEqualArrays() { - assertEquals(array().hashCode(), array().hashCode()); - assertEquals(array("foo").hashCode(), array("foo").hashCode()); - } - - @Test - public void hashCode_differsForDifferentArrays() { - assertNotEquals(array().hashCode(), array("bar").hashCode()); - assertNotEquals(array("foo").hashCode(), array("bar").hashCode()); - } - - @Test - public void canBeSerializedAndDeserialized() throws Exception { - array.add(true).add(3.14d).add(23).add("foo").add(new JsonArray().add(false)); - - assertEquals(array, TestUtil.serializeAndDeserialize(array)); - } - - @Test - public void deserializedArrayCanBeAccessed() throws Exception { - array.add(23); - - JsonArray deserializedArray = TestUtil.serializeAndDeserialize(array); - - assertEquals(23, deserializedArray.get(0).asInt()); - } - - private static JsonArray array(String... values) { - JsonArray array = new JsonArray(); - for (String value : values) { - array.add(value); - } - return array; - } + InOrder inOrder = inOrder(writer); + inOrder.verify(writer).writeArrayOpen(); + inOrder.verify(writer).writeArrayClose(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void write_withSingleValue() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + array.add(23); + + array.write(writer); + + InOrder inOrder = inOrder(writer); + inOrder.verify(writer).writeArrayOpen(); + inOrder.verify(writer).writeNumber("23"); + inOrder.verify(writer).writeArrayClose(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void write_withMultipleValues() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + array.add(23).add("foo").add(false); + + array.write(writer); + + InOrder inOrder = inOrder(writer); + inOrder.verify(writer).writeArrayOpen(); + inOrder.verify(writer).writeNumber("23"); + inOrder.verify(writer).writeArraySeparator(); + inOrder.verify(writer).writeString("foo"); + inOrder.verify(writer).writeArraySeparator(); + inOrder.verify(writer).writeLiteral("false"); + inOrder.verify(writer).writeArrayClose(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void isArray() { + assertTrue(array.isArray()); + } + + @Test + public void asArray() { + assertSame(array, array.asArray()); + } + + @Test + public void equals_trueForSameInstance() { + assertTrue(array.equals(array)); + } + + @Test + public void equals_trueForEqualArrays() { + assertTrue(array().equals(array())); + assertTrue(array("foo", "bar").equals(array("foo", "bar"))); + } + + @Test + public void equals_falseForDifferentArrays() { + assertFalse(array("foo", "bar").equals(array("foo", "bar", "baz"))); + assertFalse(array("foo", "bar").equals(array("bar", "foo"))); + } + + @Test + public void equals_falseForNull() { + assertFalse(array.equals(null)); + } + + @Test + public void equals_falseForSubclass() { + assertFalse(array.equals(new JsonArray(array) {})); + } + + @Test + public void hashCode_equalsForEqualArrays() { + assertTrue(array().hashCode() == array().hashCode()); + assertTrue(array("foo").hashCode() == array("foo").hashCode()); + } + + @Test + public void hashCode_differsForDifferentArrays() { + assertFalse(array().hashCode() == array("bar").hashCode()); + assertFalse(array("foo").hashCode() == array("bar").hashCode()); + } + + @Test + public void canBeSerializedAndDeserialized() throws Exception { + array.add(true).add(3.14d).add(23).add("foo").add(new JsonArray().add(false)); + + assertEquals(array, serializeAndDeserialize(array)); + } + + @Test + public void deserializedArrayCanBeAccessed() throws Exception { + array.add(23); + + JsonArray deserializedArray = serializeAndDeserialize(array); + + assertEquals(23, deserializedArray.get(0).asInt()); + } + + private static JsonArray array(String... values) { + JsonArray array = new JsonArray(); + for (String value : values) { + array.add(value); + } + return array; + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonLiteral_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonLiteral_Test.java index ea791a9a..3ba25e23 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonLiteral_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonLiteral_Test.java @@ -27,10 +27,10 @@ import static io.github.jopenlibs.vault.json.Json.FALSE; import static io.github.jopenlibs.vault.json.Json.NULL; import static io.github.jopenlibs.vault.json.Json.TRUE; -import static io.github.jopenlibs.vault.json.Json.value; +import static io.github.jopenlibs.vault.json.Json.array; +import static io.github.jopenlibs.vault.json.TestUtil.serializeAndDeserialize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -39,144 +39,143 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; -@SuppressWarnings("PMD") public class JsonLiteral_Test { - @Test - public void isNull() { - assertTrue(NULL.isNull()); + @Test + public void isNull() { + assertTrue(NULL.isNull()); - assertFalse(TRUE.isNull()); - assertFalse(FALSE.isNull()); - } + assertFalse(TRUE.isNull()); + assertFalse(FALSE.isNull()); + } - @Test - public void isTrue() { - assertTrue(TRUE.isTrue()); - - assertFalse(NULL.isTrue()); - assertFalse(FALSE.isTrue()); - } - - @Test - public void isFalse() { - assertTrue(FALSE.isFalse()); - - assertFalse(NULL.isFalse()); - assertFalse(TRUE.isFalse()); - } - - @Test - public void isBoolean() { - assertTrue(TRUE.isBoolean()); - assertTrue(FALSE.isBoolean()); - - assertFalse(NULL.isBoolean()); - } - - @Test - public void NULL_write() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - - NULL.write(writer); - - verify(writer).writeLiteral("null"); - verifyNoMoreInteractions(writer); - } - - @Test - public void TRUE_write() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - - TRUE.write(writer); - - verify(writer).writeLiteral("true"); - verifyNoMoreInteractions(writer); - } - - @Test - public void FALSE_write() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - - FALSE.write(writer); - - verify(writer).writeLiteral("false"); - verifyNoMoreInteractions(writer); - } - - @Test - public void NULL_toString() { - assertEquals("null", NULL.toString()); - } - - @Test - public void TRUE_toString() { - assertEquals("true", TRUE.toString()); - } - - @Test - public void FALSE_toString() { - assertEquals("false", FALSE.toString()); - } - - @Test - public void NULL_equals() { - assertEquals(NULL, NULL); - - assertNotEquals(null, NULL); - assertNotEquals(NULL, TRUE); - assertNotEquals(NULL, FALSE); - assertNotEquals(NULL, value("null")); - } - - @Test - public void TRUE_equals() { - assertEquals(TRUE, TRUE); - - assertNotEquals(null, TRUE); - assertNotEquals(TRUE, FALSE); - assertNotEquals(TRUE, Boolean.TRUE); - assertNotEquals(NULL, value("true")); - } - - @Test - public void FALSE_equals() { - assertEquals(FALSE, FALSE); - - assertNotEquals(null, FALSE); - assertNotEquals(FALSE, TRUE); - assertNotEquals(FALSE, Boolean.FALSE); - assertNotEquals(NULL, value("false")); - } - - @Test - public void NULL_isSerializable() throws Exception { - assertEquals(NULL, TestUtil.serializeAndDeserialize(NULL)); - assertTrue(TestUtil.serializeAndDeserialize(NULL).isNull()); - } - - @Test - public void TRUE_isSerializable() throws Exception { - assertEquals(TRUE, TestUtil.serializeAndDeserialize(TRUE)); - assertTrue(TestUtil.serializeAndDeserialize(TRUE).isBoolean()); - assertTrue(TestUtil.serializeAndDeserialize(TRUE).isTrue()); - } - - @Test - public void FALSE_isSerializable() throws Exception { - assertEquals(FALSE, TestUtil.serializeAndDeserialize(FALSE)); - assertTrue(TestUtil.serializeAndDeserialize(FALSE).isBoolean()); - assertTrue(TestUtil.serializeAndDeserialize(FALSE).isFalse()); - } - - @Test - public void sameAfterDeserialization() throws Exception { - JsonArray array = new JsonArray().add(NULL).add(NULL); - - JsonArray deserialized = TestUtil.serializeAndDeserialize(array); - - assertNotSame(NULL, deserialized.get(0)); - assertSame(deserialized.get(0), deserialized.get(1)); - } + @Test + public void isTrue() { + assertTrue(TRUE.isTrue()); + + assertFalse(NULL.isTrue()); + assertFalse(FALSE.isTrue()); + } + + @Test + public void isFalse() { + assertTrue(FALSE.isFalse()); + + assertFalse(NULL.isFalse()); + assertFalse(TRUE.isFalse()); + } + + @Test + public void isBoolean() { + assertTrue(TRUE.isBoolean()); + assertTrue(FALSE.isBoolean()); + + assertFalse(NULL.isBoolean()); + } + + @Test + public void NULL_write() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + + NULL.write(writer); + + verify(writer).writeLiteral("null"); + verifyNoMoreInteractions(writer); + } + + @Test + public void TRUE_write() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + + TRUE.write(writer); + + verify(writer).writeLiteral("true"); + verifyNoMoreInteractions(writer); + } + + @Test + public void FALSE_write() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + + FALSE.write(writer); + + verify(writer).writeLiteral("false"); + verifyNoMoreInteractions(writer); + } + + @Test + public void NULL_toString() { + assertEquals("null", NULL.toString()); + } + + @Test + public void TRUE_toString() { + assertEquals("true", TRUE.toString()); + } + + @Test + public void FALSE_toString() { + assertEquals("false", FALSE.toString()); + } + + @Test + public void NULL_equals() { + assertTrue(NULL.equals(NULL)); + + assertFalse(NULL.equals(null)); + assertFalse(NULL.equals(TRUE)); + assertFalse(NULL.equals(FALSE)); + assertFalse(NULL.equals(Json.value("null"))); + } + + @Test + public void TRUE_equals() { + assertTrue(TRUE.equals(TRUE)); + + assertFalse(TRUE.equals(null)); + assertFalse(TRUE.equals(FALSE)); + assertFalse(TRUE.equals(Boolean.TRUE)); + assertFalse(NULL.equals(Json.value("true"))); + } + + @Test + public void FALSE_equals() { + assertTrue(FALSE.equals(FALSE)); + + assertFalse(FALSE.equals(null)); + assertFalse(FALSE.equals(TRUE)); + assertFalse(FALSE.equals(Boolean.FALSE)); + assertFalse(NULL.equals(Json.value("false"))); + } + + @Test + public void NULL_isSerializable() throws Exception { + assertEquals(NULL, serializeAndDeserialize(NULL)); + assertTrue(serializeAndDeserialize(NULL).isNull()); + } + + @Test + public void TRUE_isSerializable() throws Exception { + assertEquals(TRUE, serializeAndDeserialize(TRUE)); + assertTrue(serializeAndDeserialize(TRUE).isBoolean()); + assertTrue(serializeAndDeserialize(TRUE).isTrue()); + } + + @Test + public void FALSE_isSerializable() throws Exception { + assertEquals(FALSE, serializeAndDeserialize(FALSE)); + assertTrue(serializeAndDeserialize(FALSE).isBoolean()); + assertTrue(serializeAndDeserialize(FALSE).isFalse()); + } + + @Test + public void sameAfterDeserialization() throws Exception { + JsonArray array = new JsonArray().add(NULL).add(NULL); + + JsonArray deserialized = serializeAndDeserialize(array); + + assertNotSame(NULL, deserialized.get(0)); + assertSame(deserialized.get(0), deserialized.get(1)); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonNumber_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonNumber_Test.java index 748c2436..24cf4e0b 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonNumber_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonNumber_Test.java @@ -26,154 +26,157 @@ import org.junit.Before; import org.junit.Test; +import static io.github.jopenlibs.vault.json.TestUtil.assertException; +import static io.github.jopenlibs.vault.json.TestUtil.serializeAndDeserialize; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -@SuppressWarnings("PMD") public class JsonNumber_Test { - private StringWriter output; - private JsonWriter writer; - - @Before - public void setUp() { - output = new StringWriter(); - writer = new JsonWriter(output); - } - - @Test - public void constructor_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "string is null", - (Runnable) () -> new JsonNumber(null)); - } - - @Test - public void write() throws IOException { - new JsonNumber("23").write(writer); - - assertEquals("23", output.toString()); - } - - @Test - public void toString_returnsInputString() { - assertEquals("foo", new JsonNumber("foo").toString()); - } - - @Test - public void isNumber() { - assertTrue(new JsonNumber("23").isNumber()); - } - - @Test - public void asInt() { - assertEquals(23, new JsonNumber("23").asInt()); - } - - @Test(expected = NumberFormatException.class) - public void asInt_failsWithExceedingValues() { - new JsonNumber("10000000000").asInt(); - } - - @Test(expected = NumberFormatException.class) - public void asInt_failsWithExponent() { - new JsonNumber("1e5").asInt(); - } - - @Test(expected = NumberFormatException.class) - public void asInt_failsWithFractional() { - new JsonNumber("23.5").asInt(); - } - - @Test - public void asLong() { - assertEquals(23L, new JsonNumber("23").asLong()); - } - - @Test(expected = NumberFormatException.class) - public void asLong_failsWithExceedingValues() { - new JsonNumber("10000000000000000000").asLong(); - } - - @Test(expected = NumberFormatException.class) - public void asLong_failsWithExponent() { - new JsonNumber("1e5").asLong(); - } - - @Test(expected = NumberFormatException.class) - public void asLong_failsWithFractional() { - new JsonNumber("23.5").asLong(); - } - - @Test - public void asFloat() { - assertEquals(23.05f, new JsonNumber("23.05").asFloat(), 0); - } - - @Test - public void asFloat_returnsInfinityForExceedingValues() { - assertEquals(Float.POSITIVE_INFINITY, new JsonNumber("1e50").asFloat(), 0); - assertEquals(Float.NEGATIVE_INFINITY, new JsonNumber("-1e50").asFloat(), 0); - } - - @Test - public void asDouble() { - double result = new JsonNumber("23.05").asDouble(); - - assertEquals(23.05, result, 0); - } - - @Test - public void asDouble_returnsInfinityForExceedingValues() { - assertEquals(Double.POSITIVE_INFINITY, new JsonNumber("1e500").asDouble(), 0); - assertEquals(Double.NEGATIVE_INFINITY, new JsonNumber("-1e500").asDouble(), 0); - } - - @Test - public void equals_trueForSameInstance() { - JsonNumber number = new JsonNumber("23"); - - assertEquals(number, number); - } - - @Test - public void equals_trueForEqualNumberStrings() { - assertEquals(new JsonNumber("23"), new JsonNumber("23")); - } - - @Test - public void equals_falseForDifferentNumberStrings() { - assertNotEquals(new JsonNumber("23"), new JsonNumber("42")); - assertNotEquals(new JsonNumber("1e+5"), new JsonNumber("1e5")); - } - - @Test - public void equals_falseForNull() { - assertNotEquals(null, new JsonNumber("23")); - } - - @Test - public void equals_falseForSubclass() { - assertNotEquals(new JsonNumber("23"), new JsonNumber("23") { - }); - } - - @Test - public void hashCode_equalsForEqualStrings() { - assertEquals(new JsonNumber("23").hashCode(), new JsonNumber("23").hashCode()); - } - - @Test - public void hashCode_differsForDifferentStrings() { - assertNotEquals(new JsonNumber("23").hashCode(), new JsonNumber("42").hashCode()); - } - - @Test - public void canBeSerializedAndDeserialized() throws Exception { - JsonNumber number = new JsonNumber("3.14"); - - assertEquals(number, TestUtil.serializeAndDeserialize(number)); - } + private StringWriter output; + private JsonWriter writer; + + @Before + public void setUp() { + output = new StringWriter(); + writer = new JsonWriter(output); + } + + @Test + public void constructor_failsWithNull() { + assertException(NullPointerException.class, "string is null", new Runnable() { + public void run() { + new JsonNumber(null); + } + }); + } + + @Test + public void write() throws IOException { + new JsonNumber("23").write(writer); + + assertEquals("23", output.toString()); + } + + @Test + public void toString_returnsInputString() { + assertEquals("foo", new JsonNumber("foo").toString()); + } + + @Test + public void isNumber() { + assertTrue(new JsonNumber("23").isNumber()); + } + + @Test + public void asInt() { + assertEquals(23, new JsonNumber("23").asInt()); + } + + @Test(expected = NumberFormatException.class) + public void asInt_failsWithExceedingValues() { + new JsonNumber("10000000000").asInt(); + } + + @Test(expected = NumberFormatException.class) + public void asInt_failsWithExponent() { + new JsonNumber("1e5").asInt(); + } + + @Test(expected = NumberFormatException.class) + public void asInt_failsWithFractional() { + new JsonNumber("23.5").asInt(); + } + + @Test + public void asLong() { + assertEquals(23l, new JsonNumber("23").asLong()); + } + + @Test(expected = NumberFormatException.class) + public void asLong_failsWithExceedingValues() { + new JsonNumber("10000000000000000000").asLong(); + } + + @Test(expected = NumberFormatException.class) + public void asLong_failsWithExponent() { + new JsonNumber("1e5").asLong(); + } + + @Test(expected = NumberFormatException.class) + public void asLong_failsWithFractional() { + new JsonNumber("23.5").asLong(); + } + + @Test + public void asFloat() { + assertEquals(23.05f, new JsonNumber("23.05").asFloat(), 0); + } + + @Test + public void asFloat_returnsInfinityForExceedingValues() { + assertEquals(Float.POSITIVE_INFINITY, new JsonNumber("1e50").asFloat(), 0); + assertEquals(Float.NEGATIVE_INFINITY, new JsonNumber("-1e50").asFloat(), 0); + } + + @Test + public void asDouble() { + double result = new JsonNumber("23.05").asDouble(); + + assertEquals(23.05, result, 0); + } + + @Test + public void asDouble_returnsInfinityForExceedingValues() { + assertEquals(Double.POSITIVE_INFINITY, new JsonNumber("1e500").asDouble(), 0); + assertEquals(Double.NEGATIVE_INFINITY, new JsonNumber("-1e500").asDouble(), 0); + } + + @Test + public void equals_trueForSameInstance() { + JsonNumber number = new JsonNumber("23"); + + assertTrue(number.equals(number)); + } + + @Test + public void equals_trueForEqualNumberStrings() { + assertTrue(new JsonNumber("23").equals(new JsonNumber("23"))); + } + + @Test + public void equals_falseForDifferentNumberStrings() { + assertFalse(new JsonNumber("23").equals(new JsonNumber("42"))); + assertFalse(new JsonNumber("1e+5").equals(new JsonNumber("1e5"))); + } + + @Test + public void equals_falseForNull() { + assertFalse(new JsonNumber("23").equals(null)); + } + + @Test + public void equals_falseForSubclass() { + assertFalse(new JsonNumber("23").equals(new JsonNumber("23") {})); + } + + @Test + public void hashCode_equalsForEqualStrings() { + assertTrue(new JsonNumber("23").hashCode() == new JsonNumber("23").hashCode()); + } + + @Test + public void hashCode_differsForDifferentStrings() { + assertFalse(new JsonNumber("23").hashCode() == new JsonNumber("42").hashCode()); + } + + @Test + public void canBeSerializedAndDeserialized() throws Exception { + JsonNumber number = new JsonNumber("3.14"); + + assertEquals(number, serializeAndDeserialize(number)); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonObject_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonObject_Test.java index b55a1e8c..4ba91b6d 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonObject_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonObject_Test.java @@ -24,6 +24,7 @@ import io.github.jopenlibs.vault.json.JsonObject.HashIndexTable; import io.github.jopenlibs.vault.json.JsonObject.Member; import java.io.IOException; +import java.io.StringReader; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; @@ -32,6 +33,8 @@ import org.junit.Test; import org.mockito.InOrder; +import static io.github.jopenlibs.vault.json.TestUtil.assertException; +import static io.github.jopenlibs.vault.json.TestUtil.serializeAndDeserialize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -42,961 +45,1073 @@ import static org.mockito.Mockito.mock; -@SuppressWarnings("PMD") public class JsonObject_Test { - private JsonObject object; + private JsonObject object; + + @Before + public void setUp() { + object = new JsonObject(); + } + + @Test + public void copyConstructor_failsWithNull() { + assertException(NullPointerException.class, "object is null", new Runnable() { + public void run() { + new JsonObject(null); + } + }); + } + + @Test + public void copyConstructor_hasSameValues() { + object.add("foo", 23); + JsonObject copy = new JsonObject(object); + + assertEquals(object.names(), copy.names()); + assertSame(object.get("foo"), copy.get("foo")); + } + + @Test + public void copyConstructor_worksOnSafeCopy() { + JsonObject copy = new JsonObject(object); + object.add("foo", 23); + + assertTrue(copy.isEmpty()); + } + + @Test + public void unmodifiableObject_hasSameValues() { + object.add("foo", 23); + JsonObject unmodifiableObject = JsonObject.unmodifiableObject(object); + + assertEquals(object.names(), unmodifiableObject.names()); + assertSame(object.get("foo"), unmodifiableObject.get("foo")); + } + + @Test + public void unmodifiableObject_reflectsChanges() { + JsonObject unmodifiableObject = JsonObject.unmodifiableObject(object); + object.add("foo", 23); + + assertEquals(object.names(), unmodifiableObject.names()); + assertSame(object.get("foo"), unmodifiableObject.get("foo")); + } + + @Test(expected = UnsupportedOperationException.class) + public void unmodifiableObject_preventsModification() { + JsonObject unmodifiableObject = JsonObject.unmodifiableObject(object); + + unmodifiableObject.add("foo", 23); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_reader() throws IOException { + assertEquals(new JsonObject(), JsonObject.readFrom(new StringReader("{}"))); + assertEquals(new JsonObject().add("a", 23), + JsonObject.readFrom(new StringReader("{ \"a\": 23 }"))); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_string() { + assertEquals(new JsonObject(), JsonObject.readFrom("{}")); + assertEquals(new JsonObject().add("a", 23), JsonObject.readFrom("{ \"a\": 23 }")); + } + + @Test(expected = ParseException.class) + @SuppressWarnings("deprecation") + public void readFrom_illegalJson() { + JsonObject.readFrom("This is not JSON"); + } + + @Test(expected = UnsupportedOperationException.class) + @SuppressWarnings("deprecation") + public void readFrom_wrongJsonType() { + JsonObject.readFrom("\"This is not a JSON object\""); + } + + @Test + public void isEmpty_trueAfterCreation() { + assertTrue(object.isEmpty()); + } + + @Test + public void isEmpty_falseAfterAdd() { + object.add("a", true); + + assertFalse(object.isEmpty()); + } + + @Test + public void size_zeroAfterCreation() { + assertEquals(0, object.size()); + } + + @Test + public void size_oneAfterAdd() { + object.add("a", true); + + assertEquals(1, object.size()); + } + + @Test + public void keyRepetition_allowsMultipleEntries() { + object.add("a", true); + object.add("a", "value"); + + assertEquals(2, object.size()); + } + + @Test + public void keyRepetition_getsLastEntry() { + object.add("a", true); + object.add("a", "value"); + + assertEquals("value", object.getString("a", "missing")); + } + + @Test + public void keyRepetition_equalityConsidersRepetitions() { + object.add("a", true); + object.add("a", "value"); + + JsonObject onlyFirstProperty = new JsonObject(); + onlyFirstProperty.add("a", true); + assertNotEquals(onlyFirstProperty, object); + + JsonObject bothProperties = new JsonObject(); + bothProperties.add("a", true); + bothProperties.add("a", "value"); + assertEquals(bothProperties, object); + } + + @Test + public void names_emptyAfterCreation() { + assertTrue(object.names().isEmpty()); + } + + @Test + public void names_containsNameAfterAdd() { + object.add("foo", true); + + List names = object.names(); + assertEquals(1, names.size()); + assertEquals("foo", names.get(0)); + } + + @Test + public void names_reflectsChanges() { + List names = object.names(); + + object.add("foo", true); + + assertEquals(1, names.size()); + assertEquals("foo", names.get(0)); + } + + @Test(expected = UnsupportedOperationException.class) + public void names_preventsModification() { + List names = object.names(); + + names.add("foo"); + } + + @Test + public void iterator_isEmptyAfterCreation() { + assertFalse(object.iterator().hasNext()); + } + + @Test + public void iterator_hasNextAfterAdd() { + object.add("a", true); + Iterator iterator = object.iterator(); + + assertTrue(iterator.hasNext()); + } + + @Test + public void iterator_nextReturnsActualValue() { + object.add("a", true); + Iterator iterator = object.iterator(); + + assertEquals(new Member("a", Json.TRUE), iterator.next()); + } + + @Test + public void iterator_nextProgressesToNextValue() { + object.add("a", true); + object.add("b", false); + Iterator iterator = object.iterator(); + + iterator.next(); + assertTrue(iterator.hasNext()); + assertEquals(new Member("b", Json.FALSE), iterator.next()); + } + + @Test(expected = NoSuchElementException.class) + public void iterator_nextFailsAtEnd() { + Iterator iterator = object.iterator(); + + iterator.next(); + } + + @Test(expected = UnsupportedOperationException.class) + public void iterator_doesNotAllowModification() { + object.add("a", 23); + Iterator iterator = object.iterator(); + iterator.next(); + + iterator.remove(); + } + + @Test(expected = ConcurrentModificationException.class) + public void iterator_detectsConcurrentModification() { + Iterator iterator = object.iterator(); + object.add("a", 23); + iterator.next(); + } + + @Test + public void get_failsWithNullName() { + assertException(NullPointerException.class, "name is null", new Runnable() { + public void run() { + object.get(null); + } + }); + } + + @Test + public void get_returnsNullForNonExistingMember() { + assertNull(object.get("foo")); + } + + @Test + public void get_returnsValueForName() { + object.add("foo", true); + + assertEquals(Json.TRUE, object.get("foo")); + } + + @Test + public void get_returnsLastValueForName() { + object.add("foo", false).add("foo", true); + + assertEquals(Json.TRUE, object.get("foo")); + } + + @Test + public void get_int_returnsValueFromMember() { + object.add("foo", 23); + + assertEquals(23, object.getInt("foo", 42)); + } + + @Test + public void get_int_returnsDefaultForMissingMember() { + assertEquals(23, object.getInt("foo", 23)); + } + + @Test + public void get_long_returnsValueFromMember() { + object.add("foo", 23l); + + assertEquals(23l, object.getLong("foo", 42l)); + } + + @Test + public void get_long_returnsDefaultForMissingMember() { + assertEquals(23l, object.getLong("foo", 23l)); + } + + @Test + public void get_float_returnsValueFromMember() { + object.add("foo", 3.14f); + + assertEquals(3.14f, object.getFloat("foo", 1.41f), 0); + } + + @Test + public void get_float_returnsDefaultForMissingMember() { + assertEquals(3.14f, object.getFloat("foo", 3.14f), 0); + } + + @Test + public void get_double_returnsValueFromMember() { + object.add("foo", 3.14); + + assertEquals(3.14, object.getDouble("foo", 1.41), 0); + } + + @Test + public void get_double_returnsDefaultForMissingMember() { + assertEquals(3.14, object.getDouble("foo", 3.14), 0); + } + + @Test + public void get_boolean_returnsValueFromMember() { + object.add("foo", true); + + assertTrue(object.getBoolean("foo", false)); + } + + @Test + public void get_boolean_returnsDefaultForMissingMember() { + assertFalse(object.getBoolean("foo", false)); + } + + @Test + public void get_string_returnsValueFromMember() { + object.add("foo", "bar"); + + assertEquals("bar", object.getString("foo", "default")); + } + + @Test + public void get_string_returnsDefaultForMissingMember() { + assertEquals("default", object.getString("foo", "default")); + } + + @Test + public void add_failsWithNullName() { + assertException(NullPointerException.class, "name is null", new Runnable() { + public void run() { + object.add(null, 23); + } + }); + } - @Before - public void setUp() { - object = new JsonObject(); - } - - @Test - public void copyConstructor_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "object is null", - (Runnable) () -> new JsonObject(null)); - } - - @Test - public void copyConstructor_hasSameValues() { - object.add("foo", 23); - JsonObject copy = new JsonObject(object); - - assertEquals(object.names(), copy.names()); - assertSame(object.get("foo"), copy.get("foo")); - } - - @Test - public void copyConstructor_worksOnSafeCopy() { - JsonObject copy = new JsonObject(object); - object.add("foo", 23); - - assertTrue(copy.isEmpty()); - } - - @Test - public void unmodifiableObject_hasSameValues() { - object.add("foo", 23); - JsonObject unmodifiableObject = JsonObject.unmodifiableObject(object); - - assertEquals(object.names(), unmodifiableObject.names()); - assertSame(object.get("foo"), unmodifiableObject.get("foo")); - } - - @Test - public void unmodifiableObject_reflectsChanges() { - JsonObject unmodifiableObject = JsonObject.unmodifiableObject(object); - object.add("foo", 23); - - assertEquals(object.names(), unmodifiableObject.names()); - assertSame(object.get("foo"), unmodifiableObject.get("foo")); - } - - @Test(expected = UnsupportedOperationException.class) - public void unmodifiableObject_preventsModification() { - JsonObject unmodifiableObject = JsonObject.unmodifiableObject(object); - - unmodifiableObject.add("foo", 23); - } - - @Test - public void isEmpty_trueAfterCreation() { - assertTrue(object.isEmpty()); - } - - @Test - public void isEmpty_falseAfterAdd() { - object.add("a", true); - - assertFalse(object.isEmpty()); - } - - @Test - public void size_zeroAfterCreation() { - assertEquals(0, object.size()); - } - - @Test - public void size_oneAfterAdd() { - object.add("a", true); - - assertEquals(1, object.size()); - } - - @Test - public void names_emptyAfterCreation() { - assertTrue(object.names().isEmpty()); - } - - @Test - public void names_containsNameAfterAdd() { - object.add("foo", true); - - List names = object.names(); - assertEquals(1, names.size()); - assertEquals("foo", names.get(0)); - } - - @Test - public void names_reflectsChanges() { - List names = object.names(); - - object.add("foo", true); - - assertEquals(1, names.size()); - assertEquals("foo", names.get(0)); - } - - @Test(expected = UnsupportedOperationException.class) - public void names_preventsModification() { - List names = object.names(); - - names.add("foo"); - } - - @Test - public void iterator_isEmptyAfterCreation() { - assertFalse(object.iterator().hasNext()); - } - - @Test - public void iterator_hasNextAfterAdd() { - object.add("a", true); - Iterator iterator = object.iterator(); - - assertTrue(iterator.hasNext()); - } - - @Test - public void iterator_nextReturnsActualValue() { - object.add("a", true); - Iterator iterator = object.iterator(); - - assertEquals(new Member("a", Json.TRUE), iterator.next()); - } - - @Test - public void iterator_nextProgressesToNextValue() { - object.add("a", true); - object.add("b", false); - Iterator iterator = object.iterator(); - - iterator.next(); - assertTrue(iterator.hasNext()); - assertEquals(new Member("b", Json.FALSE), iterator.next()); - } - - @Test(expected = NoSuchElementException.class) - public void iterator_nextFailsAtEnd() { - Iterator iterator = object.iterator(); - - iterator.next(); - } - - @Test(expected = UnsupportedOperationException.class) - public void iterator_doesNotAllowModification() { - object.add("a", 23); - Iterator iterator = object.iterator(); - iterator.next(); - - iterator.remove(); - } - - @Test(expected = ConcurrentModificationException.class) - public void iterator_detectsConcurrentModification() { - Iterator iterator = object.iterator(); - object.add("a", 23); - iterator.next(); - } - - @Test - public void get_failsWithNullName() { - TestUtil.assertException(NullPointerException.class, "name is null", - (Runnable) () -> object.get(null)); - } - - @Test - public void get_returnsNullForNonExistingMember() { - assertNull(object.get("foo")); - } - - @Test - public void get_returnsValueForName() { - object.add("foo", true); - - assertEquals(Json.TRUE, object.get("foo")); - } - - @Test - public void get_returnsLastValueForName() { - object.add("foo", false).add("foo", true); - - assertEquals(Json.TRUE, object.get("foo")); - } - - @Test - public void get_int_returnsValueFromMember() { - object.add("foo", 23); - - assertEquals(23, object.getInt("foo", 42)); - } - - @Test - public void get_int_returnsDefaultForMissingMember() { - assertEquals(23, object.getInt("foo", 23)); - } - - @Test - public void get_long_returnsValueFromMember() { - object.add("foo", 23L); - - assertEquals(23L, object.getLong("foo", 42L)); - } - - @Test - public void get_long_returnsDefaultForMissingMember() { - assertEquals(23L, object.getLong("foo", 23L)); - } - - @Test - public void get_float_returnsValueFromMember() { - object.add("foo", 3.14f); - - assertEquals(3.14f, object.getFloat("foo", 1.41f), 0); - } - - @Test - public void get_float_returnsDefaultForMissingMember() { - assertEquals(3.14f, object.getFloat("foo", 3.14f), 0); - } - - @Test - public void get_double_returnsValueFromMember() { - object.add("foo", 3.14); - - assertEquals(3.14, object.getDouble("foo", 1.41), 0); - } - - @Test - public void get_double_returnsDefaultForMissingMember() { - assertEquals(3.14, object.getDouble("foo", 3.14), 0); - } - - @Test - public void get_boolean_returnsValueFromMember() { - object.add("foo", true); - - assertTrue(object.getBoolean("foo", false)); - } - - @Test - public void get_boolean_returnsDefaultForMissingMember() { - assertFalse(object.getBoolean("foo", - false)); - } - - @Test - public void get_string_returnsValueFromMember() { - object.add("foo", "bar"); - - assertEquals("bar", object.getString("foo", "default")); - } - - @Test - public void get_string_returnsDefaultForMissingMember() { - assertEquals("default", object.getString("foo", "default")); - } - - @Test - public void add_failsWithNullName() { - TestUtil.assertException(NullPointerException.class, "name is null", - (Runnable) () -> object.add(null, - 23)); - } - - @Test - public void add_int() { - object.add("a", 23); - - assertEquals("{\"a\":23}", object.toString()); - } - - @Test - public void add_int_enablesChaining() { - assertSame(object, object.add("a", 23)); - } - - @Test - public void add_long() { - object.add("a", 23L); - - assertEquals("{\"a\":23}", object.toString()); - } - - @Test - public void add_long_enablesChaining() { - assertSame(object, object.add("a", 23L)); - } - - @Test - public void add_float() { - object.add("a", 3.14f); - - assertEquals("{\"a\":3.14}", object.toString()); - } + @Test + public void add_int() { + object.add("a", 23); - @Test - public void add_float_enablesChaining() { - assertSame(object, object.add("a", 3.14f)); - } + assertEquals("{\"a\":23}", object.toString()); + } - @Test - public void add_double() { - object.add("a", 3.14d); + @Test + public void add_int_enablesChaining() { + assertSame(object, object.add("a", 23)); + } + + @Test + public void add_long() { + object.add("a", 23l); + + assertEquals("{\"a\":23}", object.toString()); + } + + @Test + public void add_long_enablesChaining() { + assertSame(object, object.add("a", 23l)); + } + + @Test + public void add_float() { + object.add("a", 3.14f); + + assertEquals("{\"a\":3.14}", object.toString()); + } - assertEquals("{\"a\":3.14}", object.toString()); - } + @Test + public void add_float_enablesChaining() { + assertSame(object, object.add("a", 3.14f)); + } + + @Test + public void add_double() { + object.add("a", 3.14d); + + assertEquals("{\"a\":3.14}", object.toString()); + } - @Test - public void add_double_enablesChaining() { - assertSame(object, object.add("a", 3.14d)); - } + @Test + public void add_double_enablesChaining() { + assertSame(object, object.add("a", 3.14d)); + } + + @Test + public void add_boolean() { + object.add("a", true); - @Test - public void add_boolean() { - object.add("a", true); + assertEquals("{\"a\":true}", object.toString()); + } - assertEquals("{\"a\":true}", object.toString()); - } + @Test + public void add_boolean_enablesChaining() { + assertSame(object, object.add("a", true)); + } - @Test - public void add_boolean_enablesChaining() { - assertSame(object, object.add("a", true)); - } + @Test + public void add_string() { + object.add("a", "foo"); - @Test - public void add_string() { - object.add("a", "foo"); + assertEquals("{\"a\":\"foo\"}", object.toString()); + } - assertEquals("{\"a\":\"foo\"}", object.toString()); - } + @Test + public void add_string_toleratesNull() { + object.add("a", (String)null); - @Test - public void add_string_toleratesNull() { - object.add("a", (String) null); + assertEquals("{\"a\":null}", object.toString()); + } - assertEquals("{\"a\":null}", object.toString()); - } + @Test + public void add_string_enablesChaining() { + assertSame(object, object.add("a", "foo")); + } - @Test - public void add_string_enablesChaining() { - assertSame(object, object.add("a", "foo")); - } + @Test + public void add_jsonNull() { + object.add("a", Json.NULL); - @Test - public void add_jsonNull() { - object.add("a", Json.NULL); + assertEquals("{\"a\":null}", object.toString()); + } - assertEquals("{\"a\":null}", object.toString()); - } + @Test + public void add_jsonArray() { + object.add("a", new JsonArray()); - @Test - public void add_jsonArray() { - object.add("a", new JsonArray()); + assertEquals("{\"a\":[]}", object.toString()); + } - assertEquals("{\"a\":[]}", object.toString()); - } + @Test + public void add_jsonObject() { + object.add("a", new JsonObject()); - @Test - public void add_jsonObject() { - object.add("a", new JsonObject()); + assertEquals("{\"a\":{}}", object.toString()); + } - assertEquals("{\"a\":{}}", object.toString()); - } + @Test + public void add_json_enablesChaining() { + assertSame(object, object.add("a", Json.NULL)); + } - @Test - public void add_json_enablesChaining() { - assertSame(object, object.add("a", Json.NULL)); - } + @Test + public void add_json_failsWithNull() { + assertException(NullPointerException.class, "value is null", new Runnable() { + public void run() { + object.add("a", (JsonValue)null); + } + }); + } - @Test - public void add_json_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "value is null", - (Runnable) () -> object.add("a", - (JsonValue) null)); - } + @Test + public void add_json_nestedArray() { + JsonArray innerArray = new JsonArray(); + innerArray.add(23); - @Test - public void add_json_nestedArray() { - JsonArray innerArray = new JsonArray(); - innerArray.add(23); + object.add("a", innerArray); - object.add("a", innerArray); + assertEquals("{\"a\":[23]}", object.toString()); + } - assertEquals("{\"a\":[23]}", object.toString()); - } + @Test + public void add_json_nestedArray_modifiedAfterAdd() { + JsonArray innerArray = new JsonArray(); + object.add("a", innerArray); - @Test - public void add_json_nestedArray_modifiedAfterAdd() { - JsonArray innerArray = new JsonArray(); - object.add("a", innerArray); + innerArray.add(23); - innerArray.add(23); + assertEquals("{\"a\":[23]}", object.toString()); + } - assertEquals("{\"a\":[23]}", object.toString()); - } + @Test + public void add_json_nestedObject() { + JsonObject innerObject = new JsonObject(); + innerObject.add("a", 23); - @Test - public void add_json_nestedObject() { - JsonObject innerObject = new JsonObject(); - innerObject.add("a", 23); + object.add("a", innerObject); - object.add("a", innerObject); + assertEquals("{\"a\":{\"a\":23}}", object.toString()); + } - assertEquals("{\"a\":{\"a\":23}}", object.toString()); - } + @Test + public void add_json_nestedObject_modifiedAfterAdd() { + JsonObject innerObject = new JsonObject(); + object.add("a", innerObject); - @Test - public void add_json_nestedObject_modifiedAfterAdd() { - JsonObject innerObject = new JsonObject(); - object.add("a", innerObject); + innerObject.add("a", 23); - innerObject.add("a", 23); + assertEquals("{\"a\":{\"a\":23}}", object.toString()); + } - assertEquals("{\"a\":{\"a\":23}}", object.toString()); - } + @Test + public void set_int() { + object.set("a", 23); - @Test - public void set_int() { - object.set("a", 23); + assertEquals("{\"a\":23}", object.toString()); + } - assertEquals("{\"a\":23}", object.toString()); - } + @Test + public void set_int_enablesChaining() { + assertSame(object, object.set("a", 23)); + } - @Test - public void set_int_enablesChaining() { - assertSame(object, object.set("a", 23)); - } + @Test + public void set_long() { + object.set("a", 23l); - @Test - public void set_long() { - object.set("a", 23L); + assertEquals("{\"a\":23}", object.toString()); + } - assertEquals("{\"a\":23}", object.toString()); - } + @Test + public void set_long_enablesChaining() { + assertSame(object, object.set("a", 23l)); + } - @Test - public void set_long_enablesChaining() { - assertSame(object, object.set("a", 23L)); - } + @Test + public void set_float() { + object.set("a", 3.14f); - @Test - public void set_float() { - object.set("a", 3.14f); + assertEquals("{\"a\":3.14}", object.toString()); + } - assertEquals("{\"a\":3.14}", object.toString()); - } + @Test + public void set_float_enablesChaining() { + assertSame(object, object.set("a", 3.14f)); + } - @Test - public void set_float_enablesChaining() { - assertSame(object, object.set("a", 3.14f)); - } + @Test + public void set_double() { + object.set("a", 3.14d); - @Test - public void set_double() { - object.set("a", 3.14d); + assertEquals("{\"a\":3.14}", object.toString()); + } - assertEquals("{\"a\":3.14}", object.toString()); - } + @Test + public void set_double_enablesChaining() { + assertSame(object, object.set("a", 3.14d)); + } - @Test - public void set_double_enablesChaining() { - assertSame(object, object.set("a", 3.14d)); - } + @Test + public void set_boolean() { + object.set("a", true); - @Test - public void set_boolean() { - object.set("a", true); + assertEquals("{\"a\":true}", object.toString()); + } - assertEquals("{\"a\":true}", object.toString()); - } + @Test + public void set_boolean_enablesChaining() { + assertSame(object, object.set("a", true)); + } - @Test - public void set_boolean_enablesChaining() { - assertSame(object, object.set("a", true)); - } + @Test + public void set_string() { + object.set("a", "foo"); - @Test - public void set_string() { - object.set("a", "foo"); + assertEquals("{\"a\":\"foo\"}", object.toString()); + } - assertEquals("{\"a\":\"foo\"}", object.toString()); - } + @Test + public void set_string_enablesChaining() { + assertSame(object, object.set("a", "foo")); + } - @Test - public void set_string_enablesChaining() { - assertSame(object, object.set("a", "foo")); - } + @Test + public void set_jsonNull() { + object.set("a", Json.NULL); - @Test - public void set_jsonNull() { - object.set("a", Json.NULL); + assertEquals("{\"a\":null}", object.toString()); + } - assertEquals("{\"a\":null}", object.toString()); - } + @Test + public void set_jsonArray() { + object.set("a", new JsonArray()); - @Test - public void set_jsonArray() { - object.set("a", new JsonArray()); + assertEquals("{\"a\":[]}", object.toString()); + } - assertEquals("{\"a\":[]}", object.toString()); - } + @Test + public void set_jsonObject() { + object.set("a", new JsonObject()); - @Test - public void set_jsonObject() { - object.set("a", new JsonObject()); + assertEquals("{\"a\":{}}", object.toString()); + } - assertEquals("{\"a\":{}}", object.toString()); - } + @Test + public void set_json_enablesChaining() { + assertSame(object, object.set("a", Json.NULL)); + } - @Test - public void set_json_enablesChaining() { - assertSame(object, object.set("a", Json.NULL)); - } + @Test + public void set_addsElementIfMissing() { + object.set("a", Json.TRUE); - @Test - public void set_addsElementIfMissing() { - object.set("a", Json.TRUE); + assertEquals("{\"a\":true}", object.toString()); + } - assertEquals("{\"a\":true}", object.toString()); - } + @Test + public void set_modifiesElementIfExisting() { + object.add("a", Json.TRUE); - @Test - public void set_modifiesElementIfExisting() { - object.add("a", Json.TRUE); + object.set("a", Json.FALSE); - object.set("a", Json.FALSE); + assertEquals("{\"a\":false}", object.toString()); + } - assertEquals("{\"a\":false}", object.toString()); - } + @Test + public void set_modifiesLastElementIfMultipleExisting() { + object.add("a", 1); + object.add("a", 2); - @Test - public void set_modifiesLastElementIfMultipleExisting() { - object.add("a", 1); - object.add("a", 2); + object.set("a", Json.TRUE); - object.set("a", Json.TRUE); + assertEquals("{\"a\":1,\"a\":true}", object.toString()); + } - assertEquals("{\"a\":1,\"a\":true}", object.toString()); - } + @Test + public void remove_failsWithNullName() { + assertException(NullPointerException.class, "name is null", new Runnable() { + public void run() { + object.remove(null); + } + }); + } - @Test - public void remove_failsWithNullName() { - TestUtil.assertException(NullPointerException.class, "name is null", - (Runnable) () -> object.remove(null)); - } + @Test + public void remove_removesMatchingMember() { + object.add("a", 23); - @Test - public void remove_removesMatchingMember() { - object.add("a", 23); + object.remove("a"); - object.remove("a"); + assertEquals("{}", object.toString()); + } - assertEquals("{}", object.toString()); - } + @Test + public void remove_removesOnlyMatchingMember() { + object.add("a", 23); + object.add("b", 42); + object.add("c", true); - @Test - public void remove_removesOnlyMatchingMember() { - object.add("a", 23); - object.add("b", 42); - object.add("c", true); + object.remove("b"); - object.remove("b"); + assertEquals("{\"a\":23,\"c\":true}", object.toString()); + } - assertEquals("{\"a\":23,\"c\":true}", object.toString()); - } + @Test + public void remove_removesOnlyLastMatchingMember() { + object.add("a", 23); + object.add("a", 42); - @Test - public void remove_removesOnlyLastMatchingMember() { - object.add("a", 23); - object.add("a", 42); + object.remove("a"); - object.remove("a"); + assertEquals("{\"a\":23}", object.toString()); + } - assertEquals("{\"a\":23}", object.toString()); - } + @Test + public void remove_removesOnlyLastMatchingMember_afterRemove() { + object.add("a", 23); + object.remove("a"); + object.add("a", 42); + object.add("a", 47); - @Test - public void remove_removesOnlyLastMatchingMember_afterRemove() { - object.add("a", 23); - object.remove("a"); - object.add("a", 42); - object.add("a", 47); + object.remove("a"); - object.remove("a"); + assertEquals("{\"a\":42}", object.toString()); + } + + @Test + public void remove_doesNotModifyObjectWithoutMatchingMember() { + object.add("a", 23); + + object.remove("b"); + + assertEquals("{\"a\":23}", object.toString()); + } + + @Test + public void contains_findsAddedMembers(){ + object.add("f", 15f); + + assertTrue(object.contains("f")); + } + + @Test + public void contains_doesNotFindAbsentMembers(){ + assertFalse(object.contains("a")); + } + + @Test + public void contains_doesNotFindDeletedMembers(){ + object.add("a", "foo"); + + object.remove("a"); + + assertFalse(object.contains("a")); + } + + @Test + public void merge_failsWithNull() { + assertException(NullPointerException.class, "object is null", new Runnable() { + public void run() { + object.merge(null); + } + }); + } + + @Test + public void merge_appendsMembers() { + object.add("a", 1).add("b", 1); + object.merge(Json.object().add("c", 2).add("d", 2)); + + assertEquals(Json.object().add("a", 1).add("b", 1).add("c", 2).add("d", 2), object); + } + + @Test + public void merge_replacesMembers() { + object.add("a", 1).add("b", 1).add("c", 1); + object.merge(Json.object().add("b", 2).add("d", 2)); + + assertEquals(Json.object().add("a", 1).add("b", 2).add("c", 1).add("d", 2), object); + } + + @Test + public void write_empty() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + object.write(writer); + + InOrder inOrder = inOrder(writer); + inOrder.verify(writer).writeObjectOpen(); + inOrder.verify(writer).writeObjectClose(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void write_withSingleValue() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + object.add("a", 23); + + object.write(writer); + + InOrder inOrder = inOrder(writer); + inOrder.verify(writer).writeObjectOpen(); + inOrder.verify(writer).writeMemberName("a"); + inOrder.verify(writer).writeMemberSeparator(); + inOrder.verify(writer).writeNumber("23"); + inOrder.verify(writer).writeObjectClose(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void write_withMultipleValues() throws IOException { + JsonWriter writer = mock(JsonWriter.class); + object.add("a", 23); + object.add("b", 3.14f); + object.add("c", "foo"); + object.add("d", true); + object.add("e", (String)null); + + object.write(writer); + + InOrder inOrder = inOrder(writer); + inOrder.verify(writer).writeObjectOpen(); + inOrder.verify(writer).writeMemberName("a"); + inOrder.verify(writer).writeMemberSeparator(); + inOrder.verify(writer).writeNumber("23"); + inOrder.verify(writer).writeObjectSeparator(); + inOrder.verify(writer).writeMemberName("b"); + inOrder.verify(writer).writeMemberSeparator(); + inOrder.verify(writer).writeNumber("3.14"); + inOrder.verify(writer).writeObjectSeparator(); + inOrder.verify(writer).writeMemberName("c"); + inOrder.verify(writer).writeMemberSeparator(); + inOrder.verify(writer).writeString("foo"); + inOrder.verify(writer).writeObjectSeparator(); + inOrder.verify(writer).writeMemberName("d"); + inOrder.verify(writer).writeMemberSeparator(); + inOrder.verify(writer).writeLiteral("true"); + inOrder.verify(writer).writeObjectSeparator(); + inOrder.verify(writer).writeMemberName("e"); + inOrder.verify(writer).writeMemberSeparator(); + inOrder.verify(writer).writeLiteral("null"); + inOrder.verify(writer).writeObjectClose(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void isObject() { + assertTrue(object.isObject()); + } + + @Test + public void asObject() { + assertSame(object, object.asObject()); + } + + @Test + public void equals_trueForSameInstance() { + assertTrue(object.equals(object)); + } + + @Test + public void equals_trueForEqualObjects() { + assertTrue(object().equals(object())); + assertTrue(object("a", "1", "b", "2").equals(object("a", "1", "b", "2"))); + } + + @Test + public void equals_falseForDifferentObjects() { + assertFalse(object("a", "1").equals(object("a", "2"))); + assertFalse(object("a", "1").equals(object("b", "1"))); + assertFalse(object("a", "1", "b", "2").equals(object("b", "2", "a", "1"))); + } + + @Test + public void equals_falseForNull() { + assertFalse(new JsonObject().equals(null)); + } + + @Test + public void equals_falseForSubclass() { + JsonObject jsonObject = new JsonObject(); + + assertFalse(jsonObject.equals(new JsonObject(jsonObject) {})); + } + + @Test + public void hashCode_equalsForEqualObjects() { + assertTrue(object().hashCode() == object().hashCode()); + assertTrue(object("a", "1").hashCode() == object("a", "1").hashCode()); + } + + @Test + public void hashCode_differsForDifferentObjects() { + assertFalse(object().hashCode() == object("a", "1").hashCode()); + assertFalse(object("a", "1").hashCode() == object("a", "2").hashCode()); + assertFalse(object("a", "1").hashCode() == object("b", "1").hashCode()); + } + + @Test + public void indexOf_returnsNoIndexIfEmpty() { + assertEquals(-1, object.indexOf("a")); + } + + @Test + public void indexOf_returnsIndexOfMember() { + object.add("a", true); + + assertEquals(0, object.indexOf("a")); + } + + @Test + public void indexOf_returnsIndexOfLastMember() { + object.add("a", true); + object.add("a", true); + + assertEquals(1, object.indexOf("a")); + } + + @Test + public void indexOf_returnsIndexOfLastMember_afterRemove() { + object.add("a", true); + object.add("a", true); + object.remove("a"); + + assertEquals(0, object.indexOf("a")); + } + + @Test + public void indexOf_returnsUpdatedIndexAfterRemove() { + // See issue #16 + object.add("a", true); + object.add("b", true); + object.remove("a"); + + assertEquals(0, object.indexOf("b")); + } + + @Test + public void indexOf_returnsIndexOfLastMember_forBigObject() { + object.add("a", true); + // for indexes above 255, the hash index table does not return a value + for (int i = 0; i < 256; i++) { + object.add("x-" + i, 0); + } + object.add("a", true); + + assertEquals(257, object.indexOf("a")); + } + + @Test + public void hashIndexTable_copyConstructor() { + HashIndexTable original = new HashIndexTable(); + original.add("name", 23); - assertEquals("{\"a\":42}", object.toString()); - } + HashIndexTable copy = new HashIndexTable(original); + + assertEquals(23, copy.get("name")); + } - @Test - public void remove_doesNotModifyObjectWithoutMatchingMember() { - object.add("a", 23); + @Test + public void hashIndexTable_add() { + HashIndexTable indexTable = new HashIndexTable(); - object.remove("b"); + indexTable.add("name-0", 0); + indexTable.add("name-1", 1); + indexTable.add("name-fe", 0xfe); + indexTable.add("name-ff", 0xff); - assertEquals("{\"a\":23}", object.toString()); - } + assertEquals(0, indexTable.get("name-0")); + assertEquals(1, indexTable.get("name-1")); + assertEquals(0xfe, indexTable.get("name-fe")); + assertEquals(-1, indexTable.get("name-ff")); + } - @Test - public void merge_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "object is null", - (Runnable) () -> object.merge(null)); - } + @Test + public void hashIndexTable_add_overwritesPreviousValue() { + HashIndexTable indexTable = new HashIndexTable(); - @Test - public void merge_appendsMembers() { - object.add("a", 1).add("b", 1); - object.merge(Json.object().add("c", 2).add("d", 2)); + indexTable.add("name", 23); + indexTable.add("name", 42); - assertEquals(Json.object().add("a", 1).add("b", 1).add("c", 2).add("d", 2), object); - } + assertEquals(42, indexTable.get("name")); + } - @Test - public void merge_replacesMembers() { - object.add("a", 1).add("b", 1).add("c", 1); - object.merge(Json.object().add("b", 2).add("d", 2)); + @Test + public void hashIndexTable_add_clearsPreviousValueIfIndexExceeds0xff() { + HashIndexTable indexTable = new HashIndexTable(); - assertEquals(Json.object().add("a", 1).add("b", 2).add("c", 1).add("d", 2), object); - } + indexTable.add("name", 23); + indexTable.add("name", 300); - @Test - public void write_empty() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - object.write(writer); + assertEquals(-1, indexTable.get("name")); + } - InOrder inOrder = inOrder(writer); - inOrder.verify(writer).writeObjectOpen(); - inOrder.verify(writer).writeObjectClose(); - inOrder.verifyNoMoreInteractions(); - } + @Test + public void hashIndexTable_remove_deletesMapping() { + HashIndexTable indexTable = new HashIndexTable(); - @Test - public void write_withSingleValue() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - object.add("a", 23); + indexTable.add("name", 23); + indexTable.remove(23); - object.write(writer); + assertEquals(-1, indexTable.get("name")); + } - InOrder inOrder = inOrder(writer); - inOrder.verify(writer).writeObjectOpen(); - inOrder.verify(writer).writeMemberName("a"); - inOrder.verify(writer).writeMemberSeparator(); - inOrder.verify(writer).writeNumber("23"); - inOrder.verify(writer).writeObjectClose(); - inOrder.verifyNoMoreInteractions(); - } + @Test + public void hashIndexTable_remove_deletesMapping_withIndexAbove127() { + HashIndexTable indexTable = new HashIndexTable(); - @Test - public void write_withMultipleValues() throws IOException { - JsonWriter writer = mock(JsonWriter.class); - object.add("a", 23); - object.add("b", 3.14f); - object.add("c", "foo"); - object.add("d", true); - object.add("e", (String) null); - - object.write(writer); - - InOrder inOrder = inOrder(writer); - inOrder.verify(writer).writeObjectOpen(); - inOrder.verify(writer).writeMemberName("a"); - inOrder.verify(writer).writeMemberSeparator(); - inOrder.verify(writer).writeNumber("23"); - inOrder.verify(writer).writeObjectSeparator(); - inOrder.verify(writer).writeMemberName("b"); - inOrder.verify(writer).writeMemberSeparator(); - inOrder.verify(writer).writeNumber("3.14"); - inOrder.verify(writer).writeObjectSeparator(); - inOrder.verify(writer).writeMemberName("c"); - inOrder.verify(writer).writeMemberSeparator(); - inOrder.verify(writer).writeString("foo"); - inOrder.verify(writer).writeObjectSeparator(); - inOrder.verify(writer).writeMemberName("d"); - inOrder.verify(writer).writeMemberSeparator(); - inOrder.verify(writer).writeLiteral("true"); - inOrder.verify(writer).writeObjectSeparator(); - inOrder.verify(writer).writeMemberName("e"); - inOrder.verify(writer).writeMemberSeparator(); - inOrder.verify(writer).writeLiteral("null"); - inOrder.verify(writer).writeObjectClose(); - inOrder.verifyNoMoreInteractions(); - } + indexTable.add("name", 142); + indexTable.remove(142); - @Test - public void isObject() { - assertTrue(object.isObject()); - } + assertEquals(-1, indexTable.get("name")); + } - @Test - public void asObject() { - assertSame(object, object.asObject()); - } + @Test + public void hashIndexTable_remove_updatesSubsequentElements() { + HashIndexTable indexTable = new HashIndexTable(); - @Test - public void equals_trueForSameInstance() { - assertEquals(object, object); - } + indexTable.add("foo", 23); + indexTable.add("bar", 42); + indexTable.remove(23); - @Test - public void equals_trueForEqualObjects() { - assertEquals(object(), object()); - assertEquals(object("a", "1", "b", "2"), object("a", "1", "b", "2")); - } + assertEquals(41, indexTable.get("bar")); + } - @Test - public void equals_falseForDifferentObjects() { - assertNotEquals(object("a", "1"), object("a", "2")); - assertNotEquals(object("a", "1"), object("b", "1")); - assertNotEquals(object("a", "1", "b", "2"), object("b", "2", "a", "1")); - } + @Test + public void hashIndexTable_remove_updatesSubsequentElements_withIndexAbove127() { + HashIndexTable indexTable = new HashIndexTable(); - @Test - public void equals_falseForNull() { - assertNotEquals(null, new JsonObject()); - } + indexTable.add("foo", 128); + indexTable.add("bar", 142); + indexTable.remove(128); - @Test - public void equals_falseForSubclass() { - JsonObject jsonObject = new JsonObject(); + assertEquals(141, indexTable.get("bar")); + } - assertNotEquals(jsonObject, new JsonObject(jsonObject) { - }); - } + @Test + public void hashIndexTable_remove_doesNotChangePrecedingElements() { + HashIndexTable indexTable = new HashIndexTable(); - @Test - public void hashCode_equalsForEqualObjects() { - assertEquals(object().hashCode(), object().hashCode()); - assertEquals(object("a", "1").hashCode(), object("a", "1").hashCode()); - } + indexTable.add("foo", 23); + indexTable.add("bar", 128); + indexTable.remove(128); - @Test - public void hashCode_differsForDifferentObjects() { - assertNotEquals(object().hashCode(), object("a", "1").hashCode()); - assertNotEquals(object("a", "1").hashCode(), object("a", "2").hashCode()); - assertNotEquals(object("a", "1").hashCode(), object("b", "1").hashCode()); - } - - @Test - public void indexOf_returnsNoIndexIfEmpty() { - assertEquals(-1, object.indexOf("a")); - } - - @Test - public void indexOf_returnsIndexOfMember() { - object.add("a", true); - - assertEquals(0, object.indexOf("a")); - } - - @Test - public void indexOf_returnsIndexOfLastMember() { - object.add("a", true); - object.add("a", true); - - assertEquals(1, object.indexOf("a")); - } - - @Test - public void indexOf_returnsIndexOfLastMember_afterRemove() { - object.add("a", true); - object.add("a", true); - object.remove("a"); - - assertEquals(0, object.indexOf("a")); - } - - @Test - public void indexOf_returnsUpdatedIndexAfterRemove() { - // See issue #16 - object.add("a", true); - object.add("b", true); - object.remove("a"); - - assertEquals(0, object.indexOf("b")); - } - - @Test - public void indexOf_returnsIndexOfLastMember_forBigObject() { - object.add("a", true); - // for indexes above 255, the hash index table does not return a value - for (int i = 0; i < 256; i++) { - object.add("x-" + i, 0); - } - object.add("a", true); - - assertEquals(257, object.indexOf("a")); - } - - @Test - public void hashIndexTable_copyConstructor() { - HashIndexTable original = new HashIndexTable(); - original.add("name", 23); - - HashIndexTable copy = new HashIndexTable(original); - - assertEquals(23, copy.get("name")); - } - - @Test - public void hashIndexTable_add() { - HashIndexTable indexTable = new HashIndexTable(); - - indexTable.add("name-0", 0); - indexTable.add("name-1", 1); - indexTable.add("name-fe", 0xfe); - indexTable.add("name-ff", 0xff); - - assertEquals(0, indexTable.get("name-0")); - assertEquals(1, indexTable.get("name-1")); - assertEquals(0xfe, indexTable.get("name-fe")); - assertEquals(-1, indexTable.get("name-ff")); - } - - @Test - public void hashIndexTable_add_overwritesPreviousValue() { - HashIndexTable indexTable = new HashIndexTable(); - - indexTable.add("name", 23); - indexTable.add("name", 42); + assertEquals(23, indexTable.get("foo")); + } - assertEquals(42, indexTable.get("name")); - } - - @Test - public void hashIndexTable_add_clearsPreviousValueIfIndexExceeds0xff() { - HashIndexTable indexTable = new HashIndexTable(); - - indexTable.add("name", 23); - indexTable.add("name", 300); - - assertEquals(-1, indexTable.get("name")); - } - - @Test - public void hashIndexTable_remove() { - HashIndexTable indexTable = new HashIndexTable(); - - indexTable.add("name", 23); - indexTable.remove(23); - - assertEquals(-1, indexTable.get("name")); - } - - @Test - public void hashIndexTable_remove_updatesSubsequentElements() { - HashIndexTable indexTable = new HashIndexTable(); - - indexTable.add("foo", 23); - indexTable.add("bar", 42); - indexTable.remove(23); - - assertEquals(41, indexTable.get("bar")); - } - - @Test - public void hashIndexTable_remove_doesNotChangePrecedingElements() { - HashIndexTable indexTable = new HashIndexTable(); + @Test + public void canBeSerializedAndDeserialized() throws Exception { + object.add("foo", 23).add("bar", new JsonObject().add("a", 3.14d).add("b", true)); - indexTable.add("foo", 23); - indexTable.add("bar", 42); - indexTable.remove(42); + assertEquals(object, serializeAndDeserialize(object)); + } - assertEquals(23, indexTable.get("foo")); - } - - @Test - public void canBeSerializedAndDeserialized() throws Exception { - object.add("foo", 23).add("bar", new JsonObject().add("a", 3.14d).add("b", true)); + @Test + public void deserializedObjectCanBeAccessed() throws Exception { + object.add("foo", 23); - assertEquals(object, TestUtil.serializeAndDeserialize(object)); - } + JsonObject deserializedObject = serializeAndDeserialize(object); - @Test - public void deserializedObjectCanBeAccessed() throws Exception { - object.add("foo", 23); + assertEquals(23, deserializedObject.get("foo").asInt()); + } - JsonObject deserializedObject = TestUtil.serializeAndDeserialize(object); + @Test + public void member_returnsNameAndValue() { + Member member = new Member("a", Json.TRUE); - assertEquals(23, deserializedObject.get("foo").asInt()); - } - - @Test - public void member_returnsNameAndValue() { - Member member = new Member("a", Json.TRUE); - - assertEquals("a", member.getName()); - assertEquals(Json.TRUE, member.getValue()); - } + assertEquals("a", member.getName()); + assertEquals(Json.TRUE, member.getValue()); + } - @Test - public void member_equals_trueForSameInstance() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_equals_trueForSameInstance() { + Member member = new Member("a", Json.TRUE); - assertEquals(member, member); - } + assertTrue(member.equals(member)); + } - @Test - public void member_equals_trueForEqualObjects() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_equals_trueForEqualObjects() { + Member member = new Member("a", Json.TRUE); - assertEquals(member, new Member("a", Json.TRUE)); - } + assertTrue(member.equals(new Member("a", Json.TRUE))); + } - @Test - public void member_equals_falseForDifferingObjects() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_equals_falseForDifferingObjects() { + Member member = new Member("a", Json.TRUE); - assertNotEquals(member, new Member("b", Json.TRUE)); - assertNotEquals(member, new Member("a", Json.FALSE)); - } + assertFalse(member.equals(new Member("b", Json.TRUE))); + assertFalse(member.equals(new Member("a", Json.FALSE))); + } - @Test - public void member_equals_falseForNull() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_equals_falseForNull() { + Member member = new Member("a", Json.TRUE); - assertNotEquals(null, member); - } + assertFalse(member.equals(null)); + } - @Test - public void member_equals_falseForSubclass() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_equals_falseForSubclass() { + Member member = new Member("a", Json.TRUE); - assertNotEquals(member, new Member("a", Json.TRUE) { - }); - } + assertFalse(member.equals(new Member("a", Json.TRUE) {})); + } - @Test - public void member_hashCode_equalsForEqualObjects() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_hashCode_equalsForEqualObjects() { + Member member = new Member("a", Json.TRUE); - assertEquals(member.hashCode(), new Member("a", Json.TRUE).hashCode()); - } + assertTrue(member.hashCode() == new Member("a", Json.TRUE).hashCode()); + } - @Test - public void member_hashCode_differsForDifferingobjects() { - Member member = new Member("a", Json.TRUE); + @Test + public void member_hashCode_differsForDifferingobjects() { + Member member = new Member("a", Json.TRUE); - assertNotEquals(member.hashCode(), new Member("b", Json.TRUE).hashCode()); - assertNotEquals(member.hashCode(), new Member("a", Json.FALSE).hashCode()); - } + assertFalse(member.hashCode() == new Member("b", Json.TRUE).hashCode()); + assertFalse(member.hashCode() == new Member("a", Json.FALSE).hashCode()); + } - private static JsonObject object(String... namesAndValues) { - JsonObject object = new JsonObject(); - for (int i = 0; i < namesAndValues.length; i += 2) { - object.add(namesAndValues[i], namesAndValues[i + 1]); - } - return object; + private static JsonObject object(String... namesAndValues) { + JsonObject object = new JsonObject(); + for (int i = 0; i < namesAndValues.length; i += 2) { + object.add(namesAndValues[i], namesAndValues[i + 1]); } + return object; + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonParser_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonParser_Test.java index 767883a9..f19403b3 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonParser_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonParser_Test.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2015 EclipseSource. + * Copyright (c) 2013, 2016 EclipseSource. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,456 +21,852 @@ ******************************************************************************/ package io.github.jopenlibs.vault.json; +import io.github.jopenlibs.vault.json.Json.DefaultHandler; +import io.github.jopenlibs.vault.json.TestUtil.RunnableEx; import java.io.IOException; +import java.io.Reader; import java.io.StringReader; -import org.hamcrest.core.StringStartsWith; +import org.junit.Before; import org.junit.Test; +import static io.github.jopenlibs.vault.json.Json.parse; import static io.github.jopenlibs.vault.json.TestUtil.assertException; +import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; -@SuppressWarnings("PMD") public class JsonParser_Test { - @Test - public void parse_rejectsEmptyString() { - assertParseException(0, "Unexpected end of input", ""); - } - - @Test - public void parse_rejectsEmptyReader() { - ParseException exception = assertException(ParseException.class, (Runnable) () -> { - try { - new JsonParser(new StringReader("")).parse(); - } catch (IOException exception1) { - throw new RuntimeException(exception1); - } - }); - - assertEquals(0, exception.getOffset()); - assertThat(exception.getMessage(), - StringStartsWith.startsWith("Unexpected end of input at")); - } - - @Test - public void parse_acceptsArrays() { - assertEquals(new JsonArray(), parse("[]")); - } - - @Test - public void parse_acceptsObjects() { - assertEquals(new JsonObject(), parse("{}")); - } - - @Test - public void parse_acceptsStrings() { - assertEquals(new JsonString(""), parse("\"\"")); - } - - @Test - public void parse_acceptsLiterals() { - assertSame(Json.NULL, parse("null")); - } - - @Test - public void parse_stripsPadding() { - assertEquals(new JsonArray(), parse(" [ ] ")); - } - - @Test - public void parse_ignoresAllWhiteSpace() { - assertEquals(new JsonArray(), parse("\t\r\n [\t\r\n ]\t\r\n ")); - } - - @Test - public void parse_failsWithUnterminatedString() { - assertParseException(5, "Unexpected end of input", "[\"foo"); - } - - @Test - public void parse_handlesLineBreaksAndColumnsCorrectly() { - assertParseException(0, 1, 0, "!"); - assertParseException(2, 2, 0, "[\n!"); - assertParseException(3, 2, 0, "[\r\n!"); - assertParseException(6, 3, 1, "[ \n \n !"); - assertParseException(7, 2, 3, "[ \r\n \r !"); - } - - @Test - public void parse_handlesInputsThatExceedBufferSize() throws IOException { - String input = "[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47 ]"; - - JsonValue value = new JsonParser(new StringReader(input), 3).parse(); - - assertEquals("[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47]", value.toString()); - } - - @Test - public void parse_handlesStringsThatExceedBufferSize() throws IOException { - String input = "[ \"lorem ipsum dolor sit amet\" ]"; + private TestHandler handler; + private JsonParser parser; + + @Before + public void setUp() { + handler = new TestHandler(); + parser = new JsonParser(handler); + } + + @Test(expected = NullPointerException.class) + public void constructor_rejectsNullHandler() { + new JsonParser(null); + } + + @Test(expected = NullPointerException.class) + public void parse_string_rejectsNull() { + parser.parse((String)null); + } + + @Test(expected = NullPointerException.class) + public void parse_reader_rejectsNull() throws IOException { + parser.parse((Reader)null); + } + + @Test(expected = IllegalArgumentException.class) + public void parse_reader_rejectsNegativeBufferSize() throws IOException { + parser.parse(new StringReader("[]"), -1); + } + + @Test + public void parse_string_rejectsEmpty() { + assertParseException(0, "Unexpected end of input", ""); + } + + @Test + public void parse_reader_rejectsEmpty() { + ParseException exception = assertException(ParseException.class, new RunnableEx() { + public void run() throws IOException { + parser.parse(new StringReader("")); + } + }); + + assertEquals(0, exception.getLocation().offset); + assertThat(exception.getMessage(), startsWith("Unexpected end of input at")); + } + + @Test + public void parse_null() { + parser.parse("null"); + + assertEquals(join("startNull 0", + "endNull 4"), + handler.getLog()); + } + + @Test + public void parse_true() { + parser.parse("true"); + + assertEquals(join("startBoolean 0", + "endBoolean true 4"), + handler.getLog()); + } + + @Test + public void parse_false() { + parser.parse("false"); + + assertEquals(join("startBoolean 0", + "endBoolean false 5"), + handler.getLog()); + } + + @Test + public void parse_string() { + parser.parse("\"foo\""); + + assertEquals(join("startString 0", + "endString foo 5"), + handler.getLog()); + } + + @Test + public void parse_string_empty() { + parser.parse("\"\""); + + assertEquals(join("startString 0", + "endString 2"), + handler.getLog()); + } + + @Test + public void parse_number() { + parser.parse("23"); + + assertEquals(join("startNumber 0", + "endNumber 23 2"), + handler.getLog()); + } + + @Test + public void parse_number_negative() { + parser.parse("-23"); + + assertEquals(join("startNumber 0", + "endNumber -23 3"), + handler.getLog()); + } + + @Test + public void parse_number_negative_exponent() { + parser.parse("-2.3e-12"); + + assertEquals(join("startNumber 0", + "endNumber -2.3e-12 8"), + handler.getLog()); + } + + @Test + public void parse_array() { + parser.parse("[23]"); + + assertEquals(join("startArray 0", + "startArrayValue a1 1", + "startNumber 1", + "endNumber 23 3", + "endArrayValue a1 3", + "endArray a1 4"), + handler.getLog()); + } + + @Test + public void parse_array_empty() { + parser.parse("[]"); + + assertEquals(join("startArray 0", + "endArray a1 2"), + handler.getLog()); + } + + @Test + public void parse_object() { + parser.parse("{\"foo\": 23}"); + + assertEquals(join("startObject 0", + "startObjectName o1 1", + "endObjectName o1 foo 6", + "startObjectValue o1 foo 8", + "startNumber 8", + "endNumber 23 10", + "endObjectValue o1 foo 10", + "endObject o1 11"), + handler.getLog()); + } + + @Test + public void parse_object_empty() { + parser.parse("{}"); + + assertEquals(join("startObject 0", + "endObject o1 2"), + handler.getLog()); + } + + @Test + public void parse_stripsPadding() { + assertEquals(new JsonArray(), parse(" [ ] ")); + } + + @Test + public void parse_ignoresAllWhiteSpace() { + assertEquals(new JsonArray(), parse("\t\r\n [\t\r\n ]\t\r\n ")); + } + + @Test + public void parse_failsWithUnterminatedString() { + assertParseException(5, "Unexpected end of input", "[\"foo"); + } + + @Test + public void parse_lineAndColumn_onFirstLine() { + parser.parse("[]"); + + assertEquals("1:3", handler.lastLocation.toString()); + } + + @Test + public void parse_lineAndColumn_afterLF() { + parser.parse("[\n]"); + + assertEquals("2:2", handler.lastLocation.toString()); + } + + @Test + public void parse_lineAndColumn_afterCRLF() { + parser.parse("[\r\n]"); + + assertEquals("2:2", handler.lastLocation.toString()); + } + + @Test + public void parse_lineAndColumn_afterCR() { + parser.parse("[\r]"); + + assertEquals("1:4", handler.lastLocation.toString()); + } + + @Test + public void parse_handlesInputsThatExceedBufferSize() throws IOException { + DefaultHandler defHandler = new DefaultHandler(); + parser = new JsonParser(defHandler); + String input = "[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47 ]"; + + parser.parse(new StringReader(input), 3); + + assertEquals("[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47]", defHandler.getValue().toString()); + } + + @Test + public void parse_handlesStringsThatExceedBufferSize() throws IOException { + DefaultHandler defHandler = new DefaultHandler(); + parser = new JsonParser(defHandler); + String input = "[ \"lorem ipsum dolor sit amet\" ]"; + + parser.parse(new StringReader(input), 3); + + assertEquals("[\"lorem ipsum dolor sit amet\"]", defHandler.getValue().toString()); + } + + @Test + public void parse_handlesNumbersThatExceedBufferSize() throws IOException { + DefaultHandler defHandler = new DefaultHandler(); + parser = new JsonParser(defHandler); + String input = "[ 3.141592653589 ]"; + + parser.parse(new StringReader(input), 3); + + assertEquals("[3.141592653589]", defHandler.getValue().toString()); + } - JsonValue value = new JsonParser(new StringReader(input), 3).parse(); + @Test + public void parse_handlesPositionsCorrectlyWhenInputExceedsBufferSize() { + final String input = "{\n \"a\": 23,\n \"b\": 42,\n}"; - assertEquals("[\"lorem ipsum dolor sit amet\"]", value.toString()); - } - - @Test - public void parse_handlesNumbersThatExceedBufferSize() throws IOException { - String input = "[ 3.141592653589 ]"; + ParseException exception = assertException(ParseException.class, new RunnableEx() { + public void run() throws IOException { + parser.parse(new StringReader(input), 3); + } + }); - JsonValue value = new JsonParser(new StringReader(input), 3).parse(); + assertEquals(new Location(24, 4, 1), exception.getLocation()); + } - assertEquals("[3.141592653589]", value.toString()); + @Test + public void parse_failsOnTooDeeplyNestedArray() { + JsonArray array = new JsonArray(); + for (int i = 0; i < 1001; i++) { + array = new JsonArray().add(array); } + final String input = array.toString(); - @Test - public void parse_handlesPositionsCorrectlyWhenInputExceedsBufferSize() { - final String input = "{\n \"a\": 23,\n \"b\": 42,\n}"; + ParseException exception = assertException(ParseException.class, new RunnableEx() { + public void run() throws IOException { + parser.parse(input); + } + }); - ParseException exception = assertException(ParseException.class, (Runnable) () -> { - try { - new JsonParser(new StringReader(input), 3).parse(); - } catch (IOException ignored) { - } - }); + assertEquals("Nesting too deep at 1:1002", exception.getMessage()); + } - assertEquals(4, exception.getLine()); - assertEquals(0, exception.getColumn()); - assertEquals(24, exception.getOffset()); + @Test + public void parse_failsOnTooDeeplyNestedObject() { + JsonObject object = new JsonObject(); + for (int i = 0; i < 1001; i++) { + object = new JsonObject().add("foo", object); } + final String input = object.toString(); - @Test - public void arrays_empty() { - assertEquals("[]", parse("[]").toString()); - } + ParseException exception = assertException(ParseException.class, new RunnableEx() { + public void run() throws IOException { + parser.parse(input); + } + }); - @Test - public void arrays_singleValue() { - assertEquals("[23]", parse("[23]").toString()); - } + assertEquals("Nesting too deep at 1:7002", exception.getMessage()); + } - @Test - public void arrays_multipleValues() { - assertEquals("[23,42]", parse("[23,42]").toString()); + @Test + public void parse_failsOnTooDeeplyNestedMixedObject() { + JsonValue value = new JsonObject(); + for (int i = 0; i < 1001; i++) { + value = i % 2 == 0 ? new JsonArray().add(value) : new JsonObject().add("foo", value); } + final String input = value.toString(); - @Test - public void arrays_withWhitespaces() { - assertEquals("[23,42]", parse("[ 23 , 42 ]").toString()); - } + ParseException exception = assertException(ParseException.class, new RunnableEx() { + public void run() throws IOException { + parser.parse(input); + } + }); + + assertEquals("Nesting too deep at 1:4002", exception.getMessage()); + } + + @Test + public void parse_doesNotFailWithManyArrays() { + JsonArray array = new JsonArray(); + for (int i = 0; i < 1001; i++) { + array.add(new JsonArray().add(7)); + } + final String input = array.toString(); + + JsonValue result = parse(input); + + assertTrue(result.isArray()); + } + + @Test + public void parse_doesNotFailWithManyEmptyArrays() { + JsonArray array = new JsonArray(); + for (int i = 0; i < 1001; i++) { + array.add(new JsonArray()); + } + final String input = array.toString(); + + JsonValue result = parse(input); + + assertTrue(result.isArray()); + } + + @Test + public void parse_doesNotFailWithManyObjects() { + JsonArray array = new JsonArray(); + for (int i = 0; i < 1001; i++) { + array.add(new JsonObject().add("a", 7)); + } + final String input = array.toString(); + + JsonValue result = parse(input); + + assertTrue(result.isArray()); + } + + @Test + public void parse_doesNotFailWithManyEmptyObjects() { + JsonArray array = new JsonArray(); + for (int i = 0; i < 1001; i++) { + array.add(new JsonObject()); + } + final String input = array.toString(); + + JsonValue result = parse(input); + + assertTrue(result.isArray()); + } + + @Test + public void parse_canBeCalledTwice() { + parser.parse("[23]"); + + parser.parse("[42]"); + + assertEquals(join(// first run + "startArray 0", + "startArrayValue a1 1", + "startNumber 1", + "endNumber 23 3", + "endArrayValue a1 3", + "endArray a1 4", + // second run + "startArray 0", + "startArrayValue a2 1", + "startNumber 1", + "endNumber 42 3", + "endArrayValue a2 3", + "endArray a2 4"), + handler.getLog()); + } + + @Test + public void arrays_empty() { + assertEquals("[]", parse("[]").toString()); + } + + @Test + public void arrays_singleValue() { + assertEquals("[23]", parse("[23]").toString()); + } + + @Test + public void arrays_multipleValues() { + assertEquals("[23,42]", parse("[23,42]").toString()); + } + + @Test + public void arrays_withWhitespaces() { + assertEquals("[23,42]", parse("[ 23 , 42 ]").toString()); + } + + @Test + public void arrays_nested() { + assertEquals("[[23]]", parse("[[23]]").toString()); + assertEquals("[[[]]]", parse("[[[]]]").toString()); + assertEquals("[[23],42]", parse("[[23],42]").toString()); + assertEquals("[[23],[42]]", parse("[[23],[42]]").toString()); + assertEquals("[[23],[42]]", parse("[[23],[42]]").toString()); + assertEquals("[{\"foo\":[23]},{\"bar\":[42]}]", + parse("[{\"foo\":[23]},{\"bar\":[42]}]").toString()); + } + + @Test + public void arrays_illegalSyntax() { + assertParseException(1, "Expected value", "[,]"); + assertParseException(4, "Expected ',' or ']'", "[23 42]"); + assertParseException(4, "Expected value", "[23,]"); + } + + @Test + public void arrays_incomplete() { + assertParseException(1, "Unexpected end of input", "["); + assertParseException(2, "Unexpected end of input", "[ "); + assertParseException(3, "Unexpected end of input", "[23"); + assertParseException(4, "Unexpected end of input", "[23 "); + assertParseException(4, "Unexpected end of input", "[23,"); + assertParseException(5, "Unexpected end of input", "[23, "); + } + + @Test + public void objects_empty() { + assertEquals("{}", parse("{}").toString()); + } + + @Test + public void objects_singleValue() { + assertEquals("{\"foo\":23}", parse("{\"foo\":23}").toString()); + } + + @Test + public void objects_multipleValues() { + assertEquals("{\"foo\":23,\"bar\":42}", parse("{\"foo\":23,\"bar\":42}").toString()); + } + + @Test + public void objects_whitespace() { + assertEquals("{\"foo\":23,\"bar\":42}", parse("{ \"foo\" : 23, \"bar\" : 42 }").toString()); + } + + @Test + public void objects_nested() { + assertEquals("{\"foo\":{}}", parse("{\"foo\":{}}").toString()); + assertEquals("{\"foo\":{\"bar\":42}}", parse("{\"foo\":{\"bar\": 42}}").toString()); + assertEquals("{\"foo\":{\"bar\":{\"baz\":42}}}", + parse("{\"foo\":{\"bar\": {\"baz\": 42}}}").toString()); + assertEquals("{\"foo\":[{\"bar\":{\"baz\":[[42]]}}]}", + parse("{\"foo\":[{\"bar\": {\"baz\": [[42]]}}]}").toString()); + } + + @Test + public void objects_illegalSyntax() { + assertParseException(1, "Expected name", "{,}"); + assertParseException(1, "Expected name", "{:}"); + assertParseException(1, "Expected name", "{23}"); + assertParseException(4, "Expected ':'", "{\"a\"}"); + assertParseException(5, "Expected ':'", "{\"a\" \"b\"}"); + assertParseException(5, "Expected value", "{\"a\":}"); + assertParseException(8, "Expected name", "{\"a\":23,}"); + assertParseException(8, "Expected name", "{\"a\":23,42"); + } + + @Test + public void objects_incomplete() { + assertParseException(1, "Unexpected end of input", "{"); + assertParseException(2, "Unexpected end of input", "{ "); + assertParseException(2, "Unexpected end of input", "{\""); + assertParseException(4, "Unexpected end of input", "{\"a\""); + assertParseException(5, "Unexpected end of input", "{\"a\" "); + assertParseException(5, "Unexpected end of input", "{\"a\":"); + assertParseException(6, "Unexpected end of input", "{\"a\": "); + assertParseException(7, "Unexpected end of input", "{\"a\":23"); + assertParseException(8, "Unexpected end of input", "{\"a\":23 "); + assertParseException(8, "Unexpected end of input", "{\"a\":23,"); + assertParseException(9, "Unexpected end of input", "{\"a\":23, "); + } + + @Test + public void strings_emptyString_isAccepted() { + assertEquals("", parse("\"\"").asString()); + } + + @Test + public void strings_asciiCharacters_areAccepted() { + assertEquals(" ", parse("\" \"").asString()); + assertEquals("a", parse("\"a\"").asString()); + assertEquals("foo", parse("\"foo\"").asString()); + assertEquals("A2-D2", parse("\"A2-D2\"").asString()); + assertEquals("\u007f", parse("\"\u007f\"").asString()); + } + + @Test + public void strings_nonAsciiCharacters_areAccepted() { + assertEquals("Русский", parse("\"Русский\"").asString()); + assertEquals("العربية", parse("\"العربية\"").asString()); + assertEquals("日本語", parse("\"日本語\"").asString()); + } + + @Test + public void strings_controlCharacters_areRejected() { + // JSON string must not contain characters < 0x20 + assertParseException(3, "Expected valid string character", "\"--\n--\""); + assertParseException(3, "Expected valid string character", "\"--\r\n--\""); + assertParseException(3, "Expected valid string character", "\"--\t--\""); + assertParseException(3, "Expected valid string character", "\"--\u0000--\""); + assertParseException(3, "Expected valid string character", "\"--\u001f--\""); + } + + @Test + public void strings_validEscapes_areAccepted() { + // valid escapes are \" \\ \/ \b \f \n \r \t and unicode escapes + assertEquals(" \" ", parse("\" \\\" \"").asString()); + assertEquals(" \\ ", parse("\" \\\\ \"").asString()); + assertEquals(" / ", parse("\" \\/ \"").asString()); + assertEquals(" \u0008 ", parse("\" \\b \"").asString()); + assertEquals(" \u000c ", parse("\" \\f \"").asString()); + assertEquals(" \r ", parse("\" \\r \"").asString()); + assertEquals(" \n ", parse("\" \\n \"").asString()); + assertEquals(" \t ", parse("\" \\t \"").asString()); + } + + @Test + public void strings_escape_atStart() { + assertEquals("\\x", parse("\"\\\\x\"").asString()); + } + + @Test + public void strings_escape_atEnd() { + assertEquals("x\\", parse("\"x\\\\\"").asString()); + } + + @Test + public void strings_illegalEscapes_areRejected() { + assertParseException(2, "Expected valid escape sequence", "\"\\a\""); + assertParseException(2, "Expected valid escape sequence", "\"\\x\""); + assertParseException(2, "Expected valid escape sequence", "\"\\000\""); + } + + @Test + public void strings_validUnicodeEscapes_areAccepted() { + assertEquals("\u0021", parse("\"\\u0021\"").asString()); + assertEquals("\u4711", parse("\"\\u4711\"").asString()); + assertEquals("\uffff", parse("\"\\uffff\"").asString()); + assertEquals("\uabcdx", parse("\"\\uabcdx\"").asString()); + } + + @Test + public void strings_illegalUnicodeEscapes_areRejected() { + assertParseException(3, "Expected hexadecimal digit", "\"\\u \""); + assertParseException(3, "Expected hexadecimal digit", "\"\\ux\""); + assertParseException(5, "Expected hexadecimal digit", "\"\\u20 \""); + assertParseException(6, "Expected hexadecimal digit", "\"\\u000x\""); + } + + @Test + public void strings_incompleteStrings_areRejected() { + assertParseException(1, "Unexpected end of input", "\""); + assertParseException(4, "Unexpected end of input", "\"foo"); + assertParseException(5, "Unexpected end of input", "\"foo\\"); + assertParseException(6, "Unexpected end of input", "\"foo\\n"); + assertParseException(6, "Unexpected end of input", "\"foo\\u"); + assertParseException(7, "Unexpected end of input", "\"foo\\u0"); + assertParseException(9, "Unexpected end of input", "\"foo\\u000"); + assertParseException(10, "Unexpected end of input", "\"foo\\u0000"); + } + + @Test + public void numbers_integer() { + assertEquals(new JsonNumber("0"), parse("0")); + assertEquals(new JsonNumber("-0"), parse("-0")); + assertEquals(new JsonNumber("1"), parse("1")); + assertEquals(new JsonNumber("-1"), parse("-1")); + assertEquals(new JsonNumber("23"), parse("23")); + assertEquals(new JsonNumber("-23"), parse("-23")); + assertEquals(new JsonNumber("1234567890"), parse("1234567890")); + assertEquals(new JsonNumber("123456789012345678901234567890"), + parse("123456789012345678901234567890")); + } + + @Test + public void numbers_minusZero() { + // allowed by JSON, allowed by Java + JsonValue value = parse("-0"); + + assertEquals(0, value.asInt()); + assertEquals(0l, value.asLong()); + assertEquals(0f, value.asFloat(), 0); + assertEquals(0d, value.asDouble(), 0); + } + + @Test + public void numbers_decimal() { + assertEquals(new JsonNumber("0.23"), parse("0.23")); + assertEquals(new JsonNumber("-0.23"), parse("-0.23")); + assertEquals(new JsonNumber("1234567890.12345678901234567890"), + parse("1234567890.12345678901234567890")); + } + + @Test + public void numbers_withExponent() { + assertEquals(new JsonNumber("0.1e9"), parse("0.1e9")); + assertEquals(new JsonNumber("0.1e9"), parse("0.1e9")); + assertEquals(new JsonNumber("0.1E9"), parse("0.1E9")); + assertEquals(new JsonNumber("-0.23e9"), parse("-0.23e9")); + assertEquals(new JsonNumber("0.23e9"), parse("0.23e9")); + assertEquals(new JsonNumber("0.23e+9"), parse("0.23e+9")); + assertEquals(new JsonNumber("0.23e-9"), parse("0.23e-9")); + } + + @Test + public void numbers_withInvalidFormat() { + assertParseException(0, "Expected value", "+1"); + assertParseException(0, "Expected value", ".1"); + assertParseException(1, "Unexpected character", "02"); + assertParseException(2, "Unexpected character", "-02"); + assertParseException(1, "Expected digit", "-x"); + assertParseException(2, "Expected digit", "1.x"); + assertParseException(2, "Expected digit", "1ex"); + assertParseException(3, "Unexpected character", "1e1x"); + } + + @Test + public void numbers_incomplete() { + assertParseException(1, "Unexpected end of input", "-"); + assertParseException(2, "Unexpected end of input", "1."); + assertParseException(4, "Unexpected end of input", "1.0e"); + assertParseException(5, "Unexpected end of input", "1.0e-"); + } + + @Test + public void null_complete() { + assertEquals(Json.NULL, parse("null")); + } + + @Test + public void null_incomplete() { + assertParseException(1, "Unexpected end of input", "n"); + assertParseException(2, "Unexpected end of input", "nu"); + assertParseException(3, "Unexpected end of input", "nul"); + } + + @Test + public void null_withIllegalCharacter() { + assertParseException(1, "Expected 'u'", "nx"); + assertParseException(2, "Expected 'l'", "nux"); + assertParseException(3, "Expected 'l'", "nulx"); + assertParseException(4, "Unexpected character", "nullx"); + } + + @Test + public void true_complete() { + assertSame(Json.TRUE, parse("true")); + } + + @Test + public void true_incomplete() { + assertParseException(1, "Unexpected end of input", "t"); + assertParseException(2, "Unexpected end of input", "tr"); + assertParseException(3, "Unexpected end of input", "tru"); + } + + @Test + public void true_withIllegalCharacter() { + assertParseException(1, "Expected 'r'", "tx"); + assertParseException(2, "Expected 'u'", "trx"); + assertParseException(3, "Expected 'e'", "trux"); + assertParseException(4, "Unexpected character", "truex"); + } + + @Test + public void false_complete() { + assertSame(Json.FALSE, parse("false")); + } + + @Test + public void false_incomplete() { + assertParseException(1, "Unexpected end of input", "f"); + assertParseException(2, "Unexpected end of input", "fa"); + assertParseException(3, "Unexpected end of input", "fal"); + assertParseException(4, "Unexpected end of input", "fals"); + } + + @Test + public void false_withIllegalCharacter() { + assertParseException(1, "Expected 'a'", "fx"); + assertParseException(2, "Expected 'l'", "fax"); + assertParseException(3, "Expected 's'", "falx"); + assertParseException(4, "Expected 'e'", "falsx"); + assertParseException(5, "Unexpected character", "falsex"); + } + + private void assertParseException(int offset, String message, final String json) { + ParseException exception = assertException(ParseException.class, new Runnable() { + public void run() { + parser.parse(json); + } + }); + assertEquals(offset, exception.getLocation().offset); + assertThat(exception.getMessage(), startsWith(message + " at")); + } - @Test - public void arrays_nested() { - assertEquals("[[23],42]", parse("[[23],42]").toString()); + private static String join(String... strings) { + StringBuilder builder = new StringBuilder(); + for (String string : strings) { + builder.append(string).append('\n'); } + return builder.toString(); + } - @Test - public void arrays_illegalSyntax() { - assertParseException(1, "Expected value", "[,]"); - assertParseException(4, "Expected ',' or ']'", "[23 42]"); - assertParseException(4, "Expected value", "[23,]"); - } + static class TestHandler extends JsonHandler { - @Test - public void arrays_incomplete() { - assertParseException(1, "Unexpected end of input", "["); - assertParseException(2, "Unexpected end of input", "[ "); - assertParseException(3, "Unexpected end of input", "[23"); - assertParseException(4, "Unexpected end of input", "[23 "); - assertParseException(4, "Unexpected end of input", "[23,"); - assertParseException(5, "Unexpected end of input", "[23, "); - } + Location lastLocation; + StringBuilder log = new StringBuilder(); + int sequence = 0; - @Test - public void objects_empty() { - assertEquals("{}", parse("{}").toString()); + @Override + public void startNull() { + record("startNull"); } - @Test - public void objects_singleValue() { - assertEquals("{\"foo\":23}", parse("{\"foo\":23}").toString()); + @Override + public void endNull() { + record("endNull"); } - @Test - public void objects_multipleValues() { - assertEquals("{\"foo\":23,\"bar\":42}", parse("{\"foo\":23,\"bar\":42}").toString()); + @Override + public void startBoolean() { + record("startBoolean"); } - @Test - public void objects_whitespace() { - assertEquals("{\"foo\":23,\"bar\":42}", parse("{ \"foo\" : 23, \"bar\" : 42 }").toString()); + @Override + public void endBoolean(boolean value) { + record("endBoolean", Boolean.valueOf(value)); } - @Test - public void objects_nested() { - assertEquals("{\"foo\":{\"bar\":42}}", parse("{\"foo\":{\"bar\":42}}").toString()); + @Override + public void startString() { + record("startString"); } - @Test - public void objects_illegalSyntax() { - assertParseException(1, "Expected name", "{,}"); - assertParseException(1, "Expected name", "{:}"); - assertParseException(1, "Expected name", "{23}"); - assertParseException(4, "Expected ':'", "{\"a\"}"); - assertParseException(5, "Expected ':'", "{\"a\" \"b\"}"); - assertParseException(5, "Expected value", "{\"a\":}"); - assertParseException(8, "Expected name", "{\"a\":23,}"); - assertParseException(8, "Expected name", "{\"a\":23,42"); + @Override + public void endString(String string) { + record("endString", string); } - @Test - public void objects_incomplete() { - assertParseException(1, "Unexpected end of input", "{"); - assertParseException(2, "Unexpected end of input", "{ "); - assertParseException(2, "Unexpected end of input", "{\""); - assertParseException(4, "Unexpected end of input", "{\"a\""); - assertParseException(5, "Unexpected end of input", "{\"a\" "); - assertParseException(5, "Unexpected end of input", "{\"a\":"); - assertParseException(6, "Unexpected end of input", "{\"a\": "); - assertParseException(7, "Unexpected end of input", "{\"a\":23"); - assertParseException(8, "Unexpected end of input", "{\"a\":23 "); - assertParseException(8, "Unexpected end of input", "{\"a\":23,"); - assertParseException(9, "Unexpected end of input", "{\"a\":23, "); + @Override + public void startNumber() { + record("startNumber"); } - @Test - public void strings_emptyString_isAccepted() { - assertEquals("", parse("\"\"").asString()); + @Override + public void endNumber(String string) { + record("endNumber", string); } - @Test - public void strings_asciiCharacters_areAccepted() { - assertEquals(" ", parse("\" \"").asString()); - assertEquals("a", parse("\"a\"").asString()); - assertEquals("foo", parse("\"foo\"").asString()); - assertEquals("A2-D2", parse("\"A2-D2\"").asString()); - assertEquals("\u007f", parse("\"\u007f\"").asString()); + @Override + public Object startArray() { + record("startArray"); + return "a" + ++sequence; } - @Test - public void strings_nonAsciiCharacters_areAccepted() { - assertEquals("Русский", parse("\"Русский\"").asString()); - assertEquals("العربية", parse("\"العربية\"").asString()); - assertEquals("日本語", parse("\"日本語\"").asString()); + @Override + public void endArray(Object array) { + record("endArray", array); } - @Test - public void strings_controlCharacters_areRejected() { - // JSON string must not contain characters < 0x20 - assertParseException(3, "Expected valid string character", "\"--\n--\""); - assertParseException(3, "Expected valid string character", "\"--\r\n--\""); - assertParseException(3, "Expected valid string character", "\"--\t--\""); - assertParseException(3, "Expected valid string character", "\"--\u0000--\""); - assertParseException(3, "Expected valid string character", "\"--\u001f--\""); + @Override + public void startArrayValue(Object array) { + record("startArrayValue", array); } - @Test - public void strings_validEscapes_areAccepted() { - // valid escapes are \" \\ \/ \b \f \n \r \t and unicode escapes - assertEquals(" \" ", parse("\" \\\" \"").asString()); - assertEquals(" \\ ", parse("\" \\\\ \"").asString()); - assertEquals(" / ", parse("\" \\/ \"").asString()); - assertEquals(" \u0008 ", parse("\" \\b \"").asString()); - assertEquals(" \u000c ", parse("\" \\f \"").asString()); - assertEquals(" \r ", parse("\" \\r \"").asString()); - assertEquals(" \n ", parse("\" \\n \"").asString()); - assertEquals(" \t ", parse("\" \\t \"").asString()); + @Override + public void endArrayValue(Object array) { + record("endArrayValue", array); } - @Test - public void strings_escape_atStart() { - assertEquals("\\x", parse("\"\\\\x\"").asString()); + @Override + public Object startObject() { + record("startObject"); + return "o" + ++sequence; } - @Test - public void strings_escape_atEnd() { - assertEquals("x\\", parse("\"x\\\\\"").asString()); + @Override + public void endObject(Object object) { + record("endObject", object); } - @Test - public void strings_illegalEscapes_areRejected() { - assertParseException(2, "Expected valid escape sequence", "\"\\a\""); - assertParseException(2, "Expected valid escape sequence", "\"\\x\""); - assertParseException(2, "Expected valid escape sequence", "\"\\000\""); + @Override + public void startObjectName(Object object) { + record("startObjectName", object); } - @Test - public void strings_validUnicodeEscapes_areAccepted() { - assertEquals("\u0021", parse("\"\\u0021\"").asString()); - assertEquals("\u4711", parse("\"\\u4711\"").asString()); - assertEquals("\uffff", parse("\"\\uffff\"").asString()); - assertEquals("\uabcdx", parse("\"\\uabcdx\"").asString()); + @Override + public void endObjectName(Object object, String name) { + record("endObjectName", object, name); } - @Test - public void strings_illegalUnicodeEscapes_areRejected() { - assertParseException(3, "Expected hexadecimal digit", "\"\\u \""); - assertParseException(3, "Expected hexadecimal digit", "\"\\ux\""); - assertParseException(5, "Expected hexadecimal digit", "\"\\u20 \""); - assertParseException(6, "Expected hexadecimal digit", "\"\\u000x\""); + @Override + public void startObjectValue(Object object, String name) { + record("startObjectValue", object, name); } - @Test - public void strings_incompleteStrings_areRejected() { - assertParseException(1, "Unexpected end of input", "\""); - assertParseException(4, "Unexpected end of input", "\"foo"); - assertParseException(5, "Unexpected end of input", "\"foo\\"); - assertParseException(6, "Unexpected end of input", "\"foo\\n"); - assertParseException(6, "Unexpected end of input", "\"foo\\u"); - assertParseException(7, "Unexpected end of input", "\"foo\\u0"); - assertParseException(9, "Unexpected end of input", "\"foo\\u000"); - assertParseException(10, "Unexpected end of input", "\"foo\\u0000"); + @Override + public void endObjectValue(Object object, String name) { + record("endObjectValue", object, name); } - @Test - public void numbers_integer() { - assertEquals(new JsonNumber("0"), parse("0")); - assertEquals(new JsonNumber("-0"), parse("-0")); - assertEquals(new JsonNumber("1"), parse("1")); - assertEquals(new JsonNumber("-1"), parse("-1")); - assertEquals(new JsonNumber("23"), parse("23")); - assertEquals(new JsonNumber("-23"), parse("-23")); - assertEquals(new JsonNumber("1234567890"), parse("1234567890")); - assertEquals(new JsonNumber("123456789012345678901234567890"), - parse("123456789012345678901234567890")); + private void record(String event, Object... args) { + lastLocation = getLocation(); + log.append(event); + for (Object arg : args) { + log.append(' ').append(arg); + } + log.append(' ').append(lastLocation.offset).append('\n'); } - @Test - public void numbers_minusZero() { - // allowed by JSON, allowed by Java - JsonValue value = parse("-0"); - - assertEquals(0, value.asInt()); - assertEquals(0L, value.asLong()); - assertEquals(0f, value.asFloat(), 0); - assertEquals(0d, value.asDouble(), 0); + String getLog() { + return log.toString(); } - @Test - public void numbers_decimal() { - assertEquals(new JsonNumber("0.23"), parse("0.23")); - assertEquals(new JsonNumber("-0.23"), parse("-0.23")); - assertEquals(new JsonNumber("1234567890.12345678901234567890"), - parse("1234567890.12345678901234567890")); - } - - @Test - public void numbers_withExponent() { - assertEquals(new JsonNumber("0.1e9"), parse("0.1e9")); - assertEquals(new JsonNumber("0.1e9"), parse("0.1e9")); - assertEquals(new JsonNumber("0.1E9"), parse("0.1E9")); - assertEquals(new JsonNumber("-0.23e9"), parse("-0.23e9")); - assertEquals(new JsonNumber("0.23e9"), parse("0.23e9")); - assertEquals(new JsonNumber("0.23e+9"), parse("0.23e+9")); - assertEquals(new JsonNumber("0.23e-9"), parse("0.23e-9")); - } - - @Test - public void numbers_withInvalidFormat() { - assertParseException(0, "Expected value", "+1"); - assertParseException(0, "Expected value", ".1"); - assertParseException(1, "Unexpected character", "02"); - assertParseException(2, "Unexpected character", "-02"); - assertParseException(1, "Expected digit", "-x"); - assertParseException(2, "Expected digit", "1.x"); - assertParseException(2, "Expected digit", "1ex"); - assertParseException(3, "Unexpected character", "1e1x"); - } - - @Test - public void numbers_incomplete() { - assertParseException(1, "Unexpected end of input", "-"); - assertParseException(2, "Unexpected end of input", "1."); - assertParseException(4, "Unexpected end of input", "1.0e"); - assertParseException(5, "Unexpected end of input", "1.0e-"); - } - - @Test - public void null_complete() { - assertEquals(Json.NULL, parse("null")); - } - - @Test - public void null_incomplete() { - assertParseException(1, "Unexpected end of input", "n"); - assertParseException(2, "Unexpected end of input", "nu"); - assertParseException(3, "Unexpected end of input", "nul"); - } - - @Test - public void null_withIllegalCharacter() { - assertParseException(1, "Expected 'u'", "nx"); - assertParseException(2, "Expected 'l'", "nux"); - assertParseException(3, "Expected 'l'", "nulx"); - assertParseException(4, "Unexpected character", "nullx"); - } - - @Test - public void true_complete() { - assertSame(Json.TRUE, parse("true")); - } - - @Test - public void true_incomplete() { - assertParseException(1, "Unexpected end of input", "t"); - assertParseException(2, "Unexpected end of input", "tr"); - assertParseException(3, "Unexpected end of input", "tru"); - } - - @Test - public void true_withIllegalCharacter() { - assertParseException(1, "Expected 'r'", "tx"); - assertParseException(2, "Expected 'u'", "trx"); - assertParseException(3, "Expected 'e'", "trux"); - assertParseException(4, "Unexpected character", "truex"); - } - - @Test - public void false_complete() { - assertSame(Json.FALSE, parse("false")); - } - - @Test - public void false_incomplete() { - assertParseException(1, "Unexpected end of input", "f"); - assertParseException(2, "Unexpected end of input", "fa"); - assertParseException(3, "Unexpected end of input", "fal"); - assertParseException(4, "Unexpected end of input", "fals"); - } - - @Test - public void false_withIllegalCharacter() { - assertParseException(1, "Expected 'a'", "fx"); - assertParseException(2, "Expected 'l'", "fax"); - assertParseException(3, "Expected 's'", "falx"); - assertParseException(4, "Expected 'e'", "falsx"); - assertParseException(5, "Unexpected character", "falsex"); - } - - private static void assertParseException(int offset, String message, final String json) { - ParseException exception = assertException(ParseException.class, - (Runnable) () -> parse(json)); - assertEquals(offset, exception.getOffset()); - assertThat(exception.getMessage(), StringStartsWith.startsWith(message + " at")); - } - - private static void assertParseException(int offset, int line, int column, final String json) { - ParseException exception = assertException(ParseException.class, - (Runnable) () -> parse(json)); - assertEquals("offset", offset, exception.getOffset()); - assertEquals("line", line, exception.getLine()); - assertEquals("column", column, exception.getColumn()); - } - - private static JsonValue parse(String json) { - try { - return new JsonParser(json).parse(); - } catch (IOException exception) { - throw new RuntimeException(exception); - } - } + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonString_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonString_Test.java index 4db201bf..bce28176 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonString_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonString_Test.java @@ -26,98 +26,101 @@ import org.junit.Before; import org.junit.Test; +import static io.github.jopenlibs.vault.json.TestUtil.assertException; +import static io.github.jopenlibs.vault.json.TestUtil.serializeAndDeserialize; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -@SuppressWarnings("PMD") public class JsonString_Test { - private StringWriter stringWriter; - private JsonWriter jsonWriter; - - @Before - public void setUp() { - stringWriter = new StringWriter(); - jsonWriter = new JsonWriter(stringWriter); - } - - @Test - public void constructor_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "string is null", - (Runnable) () -> new JsonString(null)); - } - - @Test - public void write() throws IOException { - new JsonString("foo").write(jsonWriter); - - assertEquals("\"foo\"", stringWriter.toString()); - } - - @Test - public void write_escapesStrings() throws IOException { - new JsonString("foo\\bar").write(jsonWriter); - - assertEquals("\"foo\\\\bar\"", stringWriter.toString()); - } - - @Test - public void isString() { - assertTrue(new JsonString("foo").isString()); - } - - @Test - public void asString() { - assertEquals("foo", new JsonString("foo").asString()); - } - - @Test - public void equals_trueForSameInstance() { - JsonString string = new JsonString("foo"); - - assertEquals(string, string); - } - - @Test - public void equals_trueForEqualStrings() { - assertEquals(new JsonString("foo"), new JsonString("foo")); - } - - @Test - public void equals_falseForDifferentStrings() { - assertNotEquals(new JsonString(""), new JsonString("foo")); - assertNotEquals(new JsonString("foo"), new JsonString("bar")); - } - - @Test - public void equals_falseForNull() { - assertNotEquals(null, new JsonString("foo")); - } - - @Test - public void equals_falseForSubclass() { - assertNotEquals(new JsonString("foo"), new JsonString("foo") { - }); - } - - @Test - public void hashCode_equalsForEqualStrings() { - assertEquals(new JsonString("foo").hashCode(), new JsonString("foo").hashCode()); - } - - @Test - public void hashCode_differsForDifferentStrings() { - assertNotEquals(new JsonString("").hashCode(), new JsonString("foo").hashCode()); - assertNotEquals(new JsonString("foo").hashCode(), new JsonString("bar").hashCode()); - } - - @Test - public void canBeSerializedAndDeserialized() throws Exception { - JsonString string = new JsonString("foo"); - - assertEquals(string, TestUtil.serializeAndDeserialize(string)); - } + private StringWriter stringWriter; + private JsonWriter jsonWriter; + + @Before + public void setUp() { + stringWriter = new StringWriter(); + jsonWriter = new JsonWriter(stringWriter); + } + + @Test + public void constructor_failsWithNull() { + assertException(NullPointerException.class, "string is null", new Runnable() { + public void run() { + new JsonString(null); + } + }); + } + + @Test + public void write() throws IOException { + new JsonString("foo").write(jsonWriter); + + assertEquals("\"foo\"", stringWriter.toString()); + } + + @Test + public void write_escapesStrings() throws IOException { + new JsonString("foo\\bar").write(jsonWriter); + + assertEquals("\"foo\\\\bar\"", stringWriter.toString()); + } + + @Test + public void isString() { + assertTrue(new JsonString("foo").isString()); + } + + @Test + public void asString() { + assertEquals("foo", new JsonString("foo").asString()); + } + + @Test + public void equals_trueForSameInstance() { + JsonString string = new JsonString("foo"); + + assertTrue(string.equals(string)); + } + + @Test + public void equals_trueForEqualStrings() { + assertTrue(new JsonString("foo").equals(new JsonString("foo"))); + } + + @Test + public void equals_falseForDifferentStrings() { + assertFalse(new JsonString("").equals(new JsonString("foo"))); + assertFalse(new JsonString("foo").equals(new JsonString("bar"))); + } + + @Test + public void equals_falseForNull() { + assertFalse(new JsonString("foo").equals(null)); + } + + @Test + public void equals_falseForSubclass() { + assertFalse(new JsonString("foo").equals(new JsonString("foo") {})); + } + + @Test + public void hashCode_equalsForEqualStrings() { + assertTrue(new JsonString("foo").hashCode() == new JsonString("foo").hashCode()); + } + + @Test + public void hashCode_differsForDifferentStrings() { + assertFalse(new JsonString("").hashCode() == new JsonString("foo").hashCode()); + assertFalse(new JsonString("foo").hashCode() == new JsonString("bar").hashCode()); + } + + @Test + public void canBeSerializedAndDeserialized() throws Exception { + JsonString string = new JsonString("foo"); + + assertEquals(string, serializeAndDeserialize(string)); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonValue_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonValue_Test.java index 88f94621..0bee42c2 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonValue_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonValue_Test.java @@ -21,132 +21,238 @@ ******************************************************************************/ package io.github.jopenlibs.vault.json; +import io.github.jopenlibs.vault.json.TestUtil.RunnableEx; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import org.junit.Test; +import static io.github.jopenlibs.vault.json.TestUtil.assertException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -@SuppressWarnings("PMD") public class JsonValue_Test { - @Test - public void writeTo() throws IOException { - JsonValue value = new JsonObject(); - Writer writer = new StringWriter(); - - value.writeTo(writer); - - assertEquals("{}", writer.toString()); - } - - @Test - public void writeTo_failsWithNullWriter() { - final JsonValue value = new JsonObject(); - - TestUtil.assertException(NullPointerException.class, "writer is null", - (TestUtil.RunnableEx) () -> - value.writeTo(null, WriterConfig.MINIMAL)); - } - - @Test - public void writeTo_failsWithNullConfig() { - final JsonValue value = new JsonObject(); - - TestUtil.assertException(NullPointerException.class, "config is null", - (TestUtil.RunnableEx) () -> - value.writeTo(new StringWriter(), null)); - } - - @Test - public void toString_failsWithNullConfig() { - final JsonValue value = new JsonObject(); - - TestUtil.assertException(NullPointerException.class, "config is null", - (TestUtil.RunnableEx) () -> - value.toString(null)); - } - - @Test - public void writeTo_doesNotCloseWriter() throws IOException { - JsonValue value = new JsonObject(); - Writer writer = spy(new StringWriter()); - - value.writeTo(writer); - - verify(writer, never()).close(); - } - - @Test - public void asObject_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not an object: null", - (Runnable) Json.NULL::asObject); - } - - @Test - public void asArray_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not an array: null", - (Runnable) Json.NULL::asArray); - } - - @Test - public void asString_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not a string: null", - (Runnable) Json.NULL::asString); - } - - @Test - public void asInt_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not a number: null", - (Runnable) Json.NULL::asInt); - } - - @Test - public void asLong_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not a number: null", - (Runnable) Json.NULL::asLong); - } - - @Test - public void asFloat_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not a number: null", - (Runnable) Json.NULL::asFloat); - } - - @Test - public void asDouble_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not a number: null", - (Runnable) Json.NULL::asDouble); - } - - @Test - public void asBoolean_failsOnIncompatibleType() { - TestUtil.assertException(UnsupportedOperationException.class, "Not a boolean: null", - (Runnable) Json.NULL::asBoolean); - } - - @Test - public void isXxx_returnsFalseForIncompatibleType() { - JsonValue jsonValue = new JsonValue() { - @Override - void write(JsonWriter writer) { - } - }; - - assertFalse(jsonValue.isArray()); - assertFalse(jsonValue.isObject()); - assertFalse(jsonValue.isString()); - assertFalse(jsonValue.isNumber()); - assertFalse(jsonValue.isBoolean()); - assertFalse(jsonValue.isNull()); - assertFalse(jsonValue.isTrue()); - assertFalse(jsonValue.isFalse()); - } + @Test + @SuppressWarnings("deprecation") + public void testConstantsAreLiterals() { + assertEquals(Json.NULL, JsonValue.NULL); + assertEquals(Json.TRUE, JsonValue.TRUE); + assertEquals(Json.FALSE, JsonValue.FALSE); + } + + @Test + @SuppressWarnings("deprecation") + public void valueOf_int() { + assertEquals(Json.value(23), JsonValue.valueOf(23)); + } + + @Test + @SuppressWarnings("deprecation") + public void valueOf_long() { + assertEquals(Json.value(23l), JsonValue.valueOf(23l)); + } + + @Test + @SuppressWarnings("deprecation") + public void valueOf_float() { + assertEquals(Json.value(23.5f), JsonValue.valueOf(23.5f)); + } + + @Test + @SuppressWarnings("deprecation") + public void valueOf_double() { + assertEquals(Json.value(23.5d), JsonValue.valueOf(23.5d)); + } + + @Test + @SuppressWarnings("deprecation") + public void valueOf_boolean() { + assertSame(Json.value(true), JsonValue.valueOf(true)); + } + + @Test + @SuppressWarnings("deprecation") + public void valueOf_string() { + assertEquals(Json.value("foo"), JsonValue.valueOf("foo")); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_string() { + assertEquals(new JsonArray(), JsonValue.readFrom("[]")); + assertEquals(new JsonObject(), JsonValue.readFrom("{}")); + assertEquals(Json.value(23), JsonValue.readFrom("23")); + assertSame(Json.NULL, JsonValue.readFrom("null")); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_reader() throws IOException { + assertEquals(new JsonArray(), JsonValue.readFrom(new StringReader("[]"))); + assertEquals(new JsonObject(), JsonValue.readFrom(new StringReader("{}"))); + assertEquals(Json.value(23), JsonValue.readFrom(new StringReader("23"))); + assertSame(Json.NULL, JsonValue.readFrom(new StringReader("null"))); + } + + @Test + @SuppressWarnings("deprecation") + public void readFrom_reader_doesNotCloseReader() throws IOException { + Reader reader = spy(new StringReader("{}")); + + JsonValue.readFrom(reader); + + verify(reader, never()).close(); + } + + @Test + public void writeTo() throws IOException { + JsonValue value = new JsonObject(); + Writer writer = new StringWriter(); + + value.writeTo(writer); + + assertEquals("{}", writer.toString()); + } + + @Test + public void writeTo_failsWithNullWriter() { + final JsonValue value = new JsonObject(); + + assertException(NullPointerException.class, "writer is null", new RunnableEx() { + public void run() throws IOException { + value.writeTo(null, WriterConfig.MINIMAL); + } + }); + } + + @Test + public void writeTo_failsWithNullConfig() { + final JsonValue value = new JsonObject(); + + assertException(NullPointerException.class, "config is null", new RunnableEx() { + public void run() throws IOException { + value.writeTo(new StringWriter(), null); + } + }); + } + + @Test + public void toString_failsWithNullConfig() { + final JsonValue value = new JsonObject(); + + assertException(NullPointerException.class, "config is null", new RunnableEx() { + public void run() throws IOException { + value.toString(null); + } + }); + } + + @Test + public void writeTo_doesNotCloseWriter() throws IOException { + JsonValue value = new JsonObject(); + Writer writer = spy(new StringWriter()); + + value.writeTo(writer); + + verify(writer, never()).close(); + } + + @Test + public void asObject_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not an object: null", new Runnable() { + public void run() { + Json.NULL.asObject(); + } + }); + } + + @Test + public void asArray_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not an array: null", new Runnable() { + public void run() { + Json.NULL.asArray(); + } + }); + } + + @Test + public void asString_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not a string: null", new Runnable() { + public void run() { + Json.NULL.asString(); + } + }); + } + + @Test + public void asInt_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not a number: null", new Runnable() { + public void run() { + Json.NULL.asInt(); + } + }); + } + + @Test + public void asLong_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not a number: null", new Runnable() { + public void run() { + Json.NULL.asLong(); + } + }); + } + + @Test + public void asFloat_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not a number: null", new Runnable() { + public void run() { + Json.NULL.asFloat(); + } + }); + } + + @Test + public void asDouble_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not a number: null", new Runnable() { + public void run() { + Json.NULL.asDouble(); + } + }); + } + + @Test + public void asBoolean_failsOnIncompatibleType() { + assertException(UnsupportedOperationException.class, "Not a boolean: null", new Runnable() { + public void run() { + Json.NULL.asBoolean(); + } + }); + } + + @Test + public void isXxx_returnsFalseForIncompatibleType() { + JsonValue jsonValue = new JsonValue() { + @Override + void write(JsonWriter writer) throws IOException { + } + }; + + assertFalse(jsonValue.isArray()); + assertFalse(jsonValue.isObject()); + assertFalse(jsonValue.isString()); + assertFalse(jsonValue.isNumber()); + assertFalse(jsonValue.isBoolean()); + assertFalse(jsonValue.isNull()); + assertFalse(jsonValue.isTrue()); + assertFalse(jsonValue.isFalse()); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/JsonWriter_Test.java b/src/test/java/io/github/jopenlibs/vault/json/JsonWriter_Test.java index b7fbe3e2..7690d44a 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/JsonWriter_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/JsonWriter_Test.java @@ -29,158 +29,157 @@ import static org.junit.Assert.assertEquals; -@SuppressWarnings("PMD") public class JsonWriter_Test { - private StringWriter output; - private JsonWriter writer; + private StringWriter output; + private JsonWriter writer; - @Before - public void setUp() { - output = new StringWriter(); - writer = new JsonWriter(output); - } + @Before + public void setUp() { + output = new StringWriter(); + writer = new JsonWriter(output); + } - @Test - public void writeLiteral() throws IOException { - writer.writeLiteral("foo"); + @Test + public void writeLiteral() throws IOException { + writer.writeLiteral("foo"); - assertEquals("foo", output.toString()); - } + assertEquals("foo", output.toString()); + } - @Test - public void writeNumber() throws IOException { - writer.writeNumber("23"); + @Test + public void writeNumber() throws IOException { + writer.writeNumber("23"); - assertEquals("23", output.toString()); - } + assertEquals("23", output.toString()); + } - @Test - public void writeString_empty() throws IOException { - writer.writeString(""); + @Test + public void writeString_empty() throws IOException { + writer.writeString(""); - assertEquals("\"\"", output.toString()); - } + assertEquals("\"\"", output.toString()); + } - @Test - public void writeSting_escapesBackslashes() throws IOException { - writer.writeString("foo\\bar"); + @Test + public void writeSting_escapesBackslashes() throws IOException { + writer.writeString("foo\\bar"); - assertEquals("\"foo\\\\bar\"", output.toString()); - } + assertEquals("\"foo\\\\bar\"", output.toString()); + } - @Test - public void writeArrayParts() throws IOException { - writer.writeArrayOpen(); - writer.writeArraySeparator(); - writer.writeArrayClose(); + @Test + public void writeArrayParts() throws IOException { + writer.writeArrayOpen(); + writer.writeArraySeparator(); + writer.writeArrayClose(); - assertEquals("[,]", output.toString()); - } + assertEquals("[,]", output.toString()); + } - @Test - public void writeObjectParts() throws IOException { - writer.writeObjectOpen(); - writer.writeMemberSeparator(); - writer.writeObjectSeparator(); - writer.writeObjectClose(); + @Test + public void writeObjectParts() throws IOException { + writer.writeObjectOpen(); + writer.writeMemberSeparator(); + writer.writeObjectSeparator(); + writer.writeObjectClose(); - assertEquals("{:,}", output.toString()); - } + assertEquals("{:,}", output.toString()); + } - @Test - public void writeMemberName_empty() throws IOException { - writer.writeMemberName(""); + @Test + public void writeMemberName_empty() throws IOException { + writer.writeMemberName(""); - assertEquals("\"\"", output.toString()); - } + assertEquals("\"\"", output.toString()); + } - @Test - public void writeMemberName_escapesBackslashes() throws IOException { - writer.writeMemberName("foo\\bar"); + @Test + public void writeMemberName_escapesBackslashes() throws IOException { + writer.writeMemberName("foo\\bar"); - assertEquals("\"foo\\\\bar\"", output.toString()); - } + assertEquals("\"foo\\\\bar\"", output.toString()); + } - @Test - public void escapesQuotes() throws IOException { - writer.writeString("a\"b"); + @Test + public void escapesQuotes() throws IOException { + writer.writeString("a\"b"); - assertEquals("\"a\\\"b\"", output.toString()); - } + assertEquals("\"a\\\"b\"", output.toString()); + } - @Test - public void escapesEscapedQuotes() throws IOException { - writer.writeString("foo\\\"bar"); + @Test + public void escapesEscapedQuotes() throws IOException { + writer.writeString("foo\\\"bar"); - assertEquals("\"foo\\\\\\\"bar\"", output.toString()); - } + assertEquals("\"foo\\\\\\\"bar\"", output.toString()); + } - @Test - public void escapesNewLine() throws IOException { - writer.writeString("foo\nbar"); + @Test + public void escapesNewLine() throws IOException { + writer.writeString("foo\nbar"); - assertEquals("\"foo\\nbar\"", output.toString()); - } + assertEquals("\"foo\\nbar\"", output.toString()); + } - @Test - public void escapesWindowsNewLine() throws IOException { - writer.writeString("foo\r\nbar"); + @Test + public void escapesWindowsNewLine() throws IOException { + writer.writeString("foo\r\nbar"); - assertEquals("\"foo\\r\\nbar\"", output.toString()); - } + assertEquals("\"foo\\r\\nbar\"", output.toString()); + } - @Test - public void escapesTabs() throws IOException { - writer.writeString("foo\tbar"); + @Test + public void escapesTabs() throws IOException { + writer.writeString("foo\tbar"); - assertEquals("\"foo\\tbar\"", output.toString()); - } + assertEquals("\"foo\\tbar\"", output.toString()); + } - @Test - public void escapesSpecialCharacters() throws IOException { - writer.writeString("foo\u2028bar\u2029"); + @Test + public void escapesSpecialCharacters() throws IOException { + writer.writeString("foo\u2028bar\u2029"); - assertEquals("\"foo\\u2028bar\\u2029\"", output.toString()); - } + assertEquals("\"foo\\u2028bar\\u2029\"", output.toString()); + } - @Test - public void escapesZeroCharacter() throws IOException { - writer.writeString(string('f', 'o', 'o', (char) 0, 'b', 'a', 'r')); + @Test + public void escapesZeroCharacter() throws IOException { + writer.writeString(string('f', 'o', 'o', (char)0, 'b', 'a', 'r')); - assertEquals("\"foo\\u0000bar\"", output.toString()); - } + assertEquals("\"foo\\u0000bar\"", output.toString()); + } - @Test - public void escapesEscapeCharacter() throws IOException { - writer.writeString(string('f', 'o', 'o', (char) 27, 'b', 'a', 'r')); + @Test + public void escapesEscapeCharacter() throws IOException { + writer.writeString(string('f', 'o', 'o', (char)27, 'b', 'a', 'r')); - assertEquals("\"foo\\u001bbar\"", output.toString()); - } + assertEquals("\"foo\\u001bbar\"", output.toString()); + } - @Test - public void escapesControlCharacters() throws IOException { - writer.writeString(string((char) 1, (char) 8, (char) 15, (char) 16, (char) 31)); + @Test + public void escapesControlCharacters() throws IOException { + writer.writeString(string((char)1, (char)8, (char)15, (char)16, (char)31)); - assertEquals("\"\\u0001\\u0008\\u000f\\u0010\\u001f\"", output.toString()); - } + assertEquals("\"\\u0001\\u0008\\u000f\\u0010\\u001f\"", output.toString()); + } - @Test - public void escapesFirstChar() throws IOException { - writer.writeString(string('\\', 'x')); + @Test + public void escapesFirstChar() throws IOException { + writer.writeString(string('\\', 'x')); - assertEquals("\"\\\\x\"", output.toString()); - } + assertEquals("\"\\\\x\"", output.toString()); + } - @Test - public void escapesLastChar() throws IOException { - writer.writeString(string('x', '\\')); + @Test + public void escapesLastChar() throws IOException { + writer.writeString(string('x', '\\')); - assertEquals("\"x\\\\\"", output.toString()); - } + assertEquals("\"x\\\\\"", output.toString()); + } - private static String string(char... chars) { - return String.valueOf(chars); - } + private static String string(char... chars) { + return String.valueOf(chars); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/Json_Test.java b/src/test/java/io/github/jopenlibs/vault/json/Json_Test.java index 847db2cd..daec5a4b 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/Json_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/Json_Test.java @@ -27,217 +27,253 @@ import java.io.StringReader; import org.junit.Test; +import static io.github.jopenlibs.vault.json.TestUtil.assertException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -@SuppressWarnings("PMD") public class Json_Test { - @Test - public void literalConstants() { - assertTrue(Json.NULL.isNull()); - assertTrue(Json.TRUE.isTrue()); - assertTrue(Json.FALSE.isFalse()); - } - - @Test - public void value_int() { - assertEquals("0", Json.value(0).toString()); - assertEquals("23", Json.value(23).toString()); - assertEquals("-1", Json.value(-1).toString()); - assertEquals("2147483647", Json.value(Integer.MAX_VALUE).toString()); - assertEquals("-2147483648", Json.value(Integer.MIN_VALUE).toString()); - } - - @Test - public void value_long() { - assertEquals("0", Json.value(0l).toString()); - assertEquals("9223372036854775807", Json.value(Long.MAX_VALUE).toString()); - assertEquals("-9223372036854775808", Json.value(Long.MIN_VALUE).toString()); - } - - @Test - public void value_float() { - assertEquals("23.5", Json.value(23.5f).toString()); - assertEquals("-3.1416", Json.value(-3.1416f).toString()); - assertEquals("1.23E-6", Json.value(0.00000123f).toString()); - assertEquals("-1.23E7", Json.value(-12300000f).toString()); - } - - @Test - public void value_float_cutsOffPointZero() { - assertEquals("0", Json.value(0f).toString()); - assertEquals("-1", Json.value(-1f).toString()); - assertEquals("10", Json.value(10f).toString()); - } - - @Test - public void value_float_failsWithInfinity() { - String message = "Infinite and NaN values not permitted in JSON"; - TestUtil.assertException(IllegalArgumentException.class, message, - (Runnable) () -> Json.value(Float.POSITIVE_INFINITY)); - } - - @Test - public void value_float_failsWithNaN() { - String message = "Infinite and NaN values not permitted in JSON"; - TestUtil.assertException(IllegalArgumentException.class, message, - (Runnable) () -> Json.value(Float.NaN)); - } - - @Test - public void value_double() { - assertEquals("23.5", Json.value(23.5d).toString()); - assertEquals("3.1416", Json.value(3.1416d).toString()); - assertEquals("1.23E-6", Json.value(0.00000123d).toString()); - assertEquals("1.7976931348623157E308", Json.value(1.7976931348623157E308d).toString()); - } - - @Test - public void value_double_cutsOffPointZero() { - assertEquals("0", Json.value(0d).toString()); - assertEquals("-1", Json.value(-1d).toString()); - assertEquals("10", Json.value(10d).toString()); - } - - @Test - public void value_double_failsWithInfinity() { - String message = "Infinite and NaN values not permitted in JSON"; - TestUtil.assertException(IllegalArgumentException.class, message, - (Runnable) () -> Json.value(Double.POSITIVE_INFINITY)); - } - - @Test - public void value_double_failsWithNaN() { - String message = "Infinite and NaN values not permitted in JSON"; - TestUtil.assertException(IllegalArgumentException.class, message, - (Runnable) () -> Json.value(Double.NaN)); - } - - @Test - public void value_boolean() { - assertSame(Json.TRUE, Json.value(true)); - assertSame(Json.FALSE, Json.value(false)); - } - - @Test - public void value_string() { - assertEquals("", Json.value("").asString()); - assertEquals("Hello", Json.value("Hello").asString()); - assertEquals("\"Hello\"", Json.value("\"Hello\"").asString()); - } - - @Test - public void value_string_toleratesNull() { - assertSame(Json.NULL, Json.value((String) null)); - } - - @Test - public void array() { - assertEquals(new JsonArray(), Json.array()); - } - - @Test - public void array_int() { - assertEquals(new JsonArray().add(23), Json.array(23)); - assertEquals(new JsonArray().add(23).add(42), Json.array(23, 42)); - } - - @Test - public void array_int_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "values is null", - (Runnable) () -> Json.array((int[]) null)); - } - - @Test - public void array_long() { - assertEquals(new JsonArray().add(23l), Json.array(23l)); - assertEquals(new JsonArray().add(23l).add(42l), Json.array(23l, 42l)); - } - - @Test - public void array_long_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "values is null", - (Runnable) () -> Json.array((long[]) null)); - } - - @Test - public void array_float() { - assertEquals(new JsonArray().add(3.14f), Json.array(3.14f)); - assertEquals(new JsonArray().add(3.14f).add(1.41f), Json.array(3.14f, 1.41f)); - } - - @Test - public void array_float_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "values is null", - (Runnable) () -> Json.array((float[]) null)); - } - - @Test - public void array_double() { - assertEquals(new JsonArray().add(3.14d), Json.array(3.14d)); - assertEquals(new JsonArray().add(3.14d).add(1.41d), Json.array(3.14d, 1.41d)); - } - - @Test - public void array_double_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "values is null", - (Runnable) () -> Json.array((double[]) null)); - } - - @Test - public void array_boolean() { - assertEquals(new JsonArray().add(true), Json.array(true)); - assertEquals(new JsonArray().add(true).add(false), Json.array(true, false)); - } - - @Test - public void array_boolean_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "values is null", - (Runnable) () -> Json.array((boolean[]) null)); - } - - @Test - public void array_string() { - assertEquals(new JsonArray().add("foo"), Json.array("foo")); - assertEquals(new JsonArray().add("foo").add("bar"), Json.array("foo", "bar")); - } - - @Test - public void array_string_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "values is null", - (Runnable) () -> Json.array((String[]) null)); - } - - @Test - public void object() { - assertEquals(new JsonObject(), Json.object()); - } - - @Test - public void parse_string() { - assertEquals(Json.value(23), Json.parse("23")); - } - - @Test - public void parse_string_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "string is null", - (Runnable) () -> Json.parse((String) null)); - } - - @Test - public void parse_reader() throws IOException { - Reader reader = new StringReader("23"); - - assertEquals(Json.value(23), Json.parse(reader)); - } - - @Test - public void parse_reader_failsWithNull() { - TestUtil.assertException(NullPointerException.class, "reader is null", - (RunnableEx) () -> Json.parse((Reader) null)); - } + @Test + public void literalConstants() { + assertTrue(Json.NULL.isNull()); + assertTrue(Json.TRUE.isTrue()); + assertTrue(Json.FALSE.isFalse()); + } + + @Test + public void value_int() { + assertEquals("0", Json.value(0).toString()); + assertEquals("23", Json.value(23).toString()); + assertEquals("-1", Json.value(-1).toString()); + assertEquals("2147483647", Json.value(Integer.MAX_VALUE).toString()); + assertEquals("-2147483648", Json.value(Integer.MIN_VALUE).toString()); + } + + @Test + public void value_long() { + assertEquals("0", Json.value(0l).toString()); + assertEquals("9223372036854775807", Json.value(Long.MAX_VALUE).toString()); + assertEquals("-9223372036854775808", Json.value(Long.MIN_VALUE).toString()); + } + + @Test + public void value_float() { + assertEquals("23.5", Json.value(23.5f).toString()); + assertEquals("-3.1416", Json.value(-3.1416f).toString()); + assertEquals("1.23E-6", Json.value(0.00000123f).toString()); + assertEquals("-1.23E7", Json.value(-12300000f).toString()); + } + + @Test + public void value_float_cutsOffPointZero() { + assertEquals("0", Json.value(0f).toString()); + assertEquals("-1", Json.value(-1f).toString()); + assertEquals("10", Json.value(10f).toString()); + } + + @Test + public void value_float_failsWithInfinity() { + String message = "Infinite and NaN values not permitted in JSON"; + assertException(IllegalArgumentException.class, message, new Runnable() { + public void run() { + Json.value(Float.POSITIVE_INFINITY); + } + }); + } + + @Test + public void value_float_failsWithNaN() { + String message = "Infinite and NaN values not permitted in JSON"; + assertException(IllegalArgumentException.class, message, new Runnable() { + public void run() { + Json.value(Float.NaN); + } + }); + } + + @Test + public void value_double() { + assertEquals("23.5", Json.value(23.5d).toString()); + assertEquals("3.1416", Json.value(3.1416d).toString()); + assertEquals("1.23E-6", Json.value(0.00000123d).toString()); + assertEquals("1.7976931348623157E308", Json.value(1.7976931348623157E308d).toString()); + } + + @Test + public void value_double_cutsOffPointZero() { + assertEquals("0", Json.value(0d).toString()); + assertEquals("-1", Json.value(-1d).toString()); + assertEquals("10", Json.value(10d).toString()); + } + + @Test + public void value_double_failsWithInfinity() { + String message = "Infinite and NaN values not permitted in JSON"; + assertException(IllegalArgumentException.class, message, new Runnable() { + public void run() { + Json.value(Double.POSITIVE_INFINITY); + } + }); + } + + @Test + public void value_double_failsWithNaN() { + String message = "Infinite and NaN values not permitted in JSON"; + assertException(IllegalArgumentException.class, message, new Runnable() { + public void run() { + Json.value(Double.NaN); + } + }); + } + + @Test + public void value_boolean() { + assertSame(Json.TRUE, Json.value(true)); + assertSame(Json.FALSE, Json.value(false)); + } + + @Test + public void value_string() { + assertEquals("", Json.value("").asString()); + assertEquals("Hello", Json.value("Hello").asString()); + assertEquals("\"Hello\"", Json.value("\"Hello\"").asString()); + } + + @Test + public void value_string_toleratesNull() { + assertSame(Json.NULL, Json.value(null)); + } + + @Test + public void array() { + assertEquals(new JsonArray(), Json.array()); + } + + @Test + public void array_int() { + assertEquals(new JsonArray().add(23), Json.array(23)); + assertEquals(new JsonArray().add(23).add(42), Json.array(23, 42)); + } + + @Test + public void array_int_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "values is null", new Runnable() { + public void run() { + Json.array((int[])null); + } + }); + } + + @Test + public void array_long() { + assertEquals(new JsonArray().add(23l), Json.array(23l)); + assertEquals(new JsonArray().add(23l).add(42l), Json.array(23l, 42l)); + } + + @Test + public void array_long_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "values is null", new Runnable() { + public void run() { + Json.array((long[])null); + } + }); + } + + @Test + public void array_float() { + assertEquals(new JsonArray().add(3.14f), Json.array(3.14f)); + assertEquals(new JsonArray().add(3.14f).add(1.41f), Json.array(3.14f, 1.41f)); + } + + @Test + public void array_float_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "values is null", new Runnable() { + public void run() { + Json.array((float[])null); + } + }); + } + + @Test + public void array_double() { + assertEquals(new JsonArray().add(3.14d), Json.array(3.14d)); + assertEquals(new JsonArray().add(3.14d).add(1.41d), Json.array(3.14d, 1.41d)); + } + + @Test + public void array_double_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "values is null", new Runnable() { + public void run() { + Json.array((double[])null); + } + }); + } + + @Test + public void array_boolean() { + assertEquals(new JsonArray().add(true), Json.array(true)); + assertEquals(new JsonArray().add(true).add(false), Json.array(true, false)); + } + + @Test + public void array_boolean_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "values is null", new Runnable() { + public void run() { + Json.array((boolean[])null); + } + }); + } + + @Test + public void array_string() { + assertEquals(new JsonArray().add("foo"), Json.array("foo")); + assertEquals(new JsonArray().add("foo").add("bar"), Json.array("foo", "bar")); + } + + @Test + public void array_string_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "values is null", new Runnable() { + public void run() { + Json.array((String[])null); + } + }); + } + + @Test + public void object() { + assertEquals(new JsonObject(), Json.object()); + } + + @Test + public void parse_string() { + assertEquals(Json.value(23), Json.parse("23")); + } + + @Test + public void parse_string_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "string is null", new Runnable() { + public void run() { + Json.parse((String)null); + } + }); + } + + @Test + public void parse_reader() throws IOException { + Reader reader = new StringReader("23"); + + assertEquals(Json.value(23), Json.parse(reader)); + } + + @Test + public void parse_reader_failsWithNull() { + TestUtil.assertException(NullPointerException.class, "reader is null", new RunnableEx() { + public void run() throws IOException { + Json.parse((Reader)null); + } + }); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/ParseException_Test.java b/src/test/java/io/github/jopenlibs/vault/json/ParseException_Test.java index 4afd1f7f..dfb0d7be 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/ParseException_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/ParseException_Test.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2015 EclipseSource. + * Copyright (c) 2013, 2016 EclipseSource. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,28 +21,44 @@ ******************************************************************************/ package io.github.jopenlibs.vault.json; +import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; -@SuppressWarnings("PMD") public class ParseException_Test { - @Test - public void position() { - ParseException exception = new ParseException("Foo", 17, 23, 42); + private Location location; - assertEquals(17, exception.getOffset()); - assertEquals(23, exception.getLine()); - assertEquals(42, exception.getColumn()); - } + @Before + public void setUp() { + location = new Location(4711, 23, 42); + } - @Test - public void message() { - ParseException exception = new ParseException("Foo", 17, 23, 42); + @Test + public void location() { + ParseException exception = new ParseException("Foo", location); - assertEquals("Foo at 23:42", exception.getMessage()); - } + assertSame(location, exception.getLocation()); + } + + @Test + @SuppressWarnings("deprecation") + public void position() { + ParseException exception = new ParseException("Foo", location); + + assertEquals(location.offset, exception.getOffset()); + assertEquals(location.line, exception.getLine()); + assertEquals(location.column, exception.getColumn()); + } + + @Test + public void message() { + ParseException exception = new ParseException("Foo", location); + + assertEquals("Foo at 23:42", exception.getMessage()); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/PrettyPrint_Test.java b/src/test/java/io/github/jopenlibs/vault/json/PrettyPrint_Test.java index 0d132ba2..4f6b6b91 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/PrettyPrint_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/PrettyPrint_Test.java @@ -38,140 +38,139 @@ import static org.mockito.Mockito.mock; -@SuppressWarnings("PMD") public class PrettyPrint_Test { - private StringWriter output; + private StringWriter output; - @Before - public void setUp() { - output = new StringWriter(); - } + @Before + public void setUp() { + output = new StringWriter(); + } - @Test - public void testIndentWithSpaces_emptyArray() throws IOException { - new JsonArray().writeTo(output, indentWithSpaces(2)); + @Test + public void testIndentWithSpaces_emptyArray() throws IOException { + new JsonArray().writeTo(output, indentWithSpaces(2)); - assertEquals("[\n \n]", output.toString()); - } + assertEquals("[\n \n]", output.toString()); + } - @Test - public void testIndentWithSpaces_emptyObject() throws IOException { - new JsonObject().writeTo(output, indentWithSpaces(2)); + @Test + public void testIndentWithSpaces_emptyObject() throws IOException { + new JsonObject().writeTo(output, indentWithSpaces(2)); - assertEquals("{\n \n}", output.toString()); - } + assertEquals("{\n \n}", output.toString()); + } - @Test - public void testIndentWithSpaces_array() throws IOException { - new JsonArray().add(23).add(42).writeTo(output, indentWithSpaces(2)); + @Test + public void testIndentWithSpaces_array() throws IOException { + new JsonArray().add(23).add(42).writeTo(output, indentWithSpaces(2)); - assertEquals("[\n 23,\n 42\n]", output.toString()); - } + assertEquals("[\n 23,\n 42\n]", output.toString()); + } - @Test - public void testIndentWithSpaces_nestedArray() throws IOException { - new JsonArray().add(23) - .add(new JsonArray().add(42)) - .writeTo(output, indentWithSpaces(2)); + @Test + public void testIndentWithSpaces_nestedArray() throws IOException { + new JsonArray().add(23) + .add(new JsonArray().add(42)) + .writeTo(output, indentWithSpaces(2)); - assertEquals("[\n 23,\n [\n 42\n ]\n]", output.toString()); - } + assertEquals("[\n 23,\n [\n 42\n ]\n]", output.toString()); + } - @Test - public void testIndentWithSpaces_object() throws IOException { - new JsonObject().add("a", 23).add("b", 42).writeTo(output, indentWithSpaces(2)); + @Test + public void testIndentWithSpaces_object() throws IOException { + new JsonObject().add("a", 23).add("b", 42).writeTo(output, indentWithSpaces(2)); - assertEquals("{\n \"a\": 23,\n \"b\": 42\n}", output.toString()); - } + assertEquals("{\n \"a\": 23,\n \"b\": 42\n}", output.toString()); + } - @Test - public void testIndentWithSpaces_nestedObject() throws IOException { - new JsonObject().add("a", 23) - .add("b", new JsonObject().add("c", 42)) - .writeTo(output, indentWithSpaces(2)); + @Test + public void testIndentWithSpaces_nestedObject() throws IOException { + new JsonObject().add("a", 23) + .add("b", new JsonObject().add("c", 42)) + .writeTo(output, indentWithSpaces(2)); - assertEquals("{\n \"a\": 23,\n \"b\": {\n \"c\": 42\n }\n}", output.toString()); - } + assertEquals("{\n \"a\": 23,\n \"b\": {\n \"c\": 42\n }\n}", output.toString()); + } - @Test - public void testIndentWithSpaces_zero() throws IOException { - new JsonArray().add(23).add(42).writeTo(output, indentWithSpaces(0)); + @Test + public void testIndentWithSpaces_zero() throws IOException { + new JsonArray().add(23).add(42).writeTo(output, indentWithSpaces(0)); - assertEquals("[\n23,\n42\n]", output.toString()); - } + assertEquals("[\n23,\n42\n]", output.toString()); + } - @Test - public void testIndentWithSpaces_one() throws IOException { - new JsonArray().add(23).add(42).writeTo(output, indentWithSpaces(1)); + @Test + public void testIndentWithSpaces_one() throws IOException { + new JsonArray().add(23).add(42).writeTo(output, indentWithSpaces(1)); - assertEquals("[\n 23,\n 42\n]", output.toString()); - } + assertEquals("[\n 23,\n 42\n]", output.toString()); + } - @Test - public void testIndentWithSpaces_failsWithNegativeValues() { - try { - indentWithSpaces(-1); - fail(); - } catch (IllegalArgumentException ex) { - assertTrue(ex.getMessage().toLowerCase(US).contains("negative")); - } + @Test + public void testIndentWithSpaces_failsWithNegativeValues() { + try { + indentWithSpaces(-1); + fail(); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().toLowerCase(US).contains("negative")); } + } - @Test - public void testIndentWithSpaces_createsIndependentInstances() { - Writer writer = mock(Writer.class); + @Test + public void testIndentWithSpaces_createsIndependentInstances() { + Writer writer = mock(Writer.class); - WriterConfig config = indentWithSpaces(1); - Object instance1 = config.createWriter(writer); - Object instance2 = config.createWriter(writer); + WriterConfig config = indentWithSpaces(1); + Object instance1 = config.createWriter(writer); + Object instance2 = config.createWriter(writer); - assertNotSame(instance1, instance2); - } + assertNotSame(instance1, instance2); + } - @Test - public void testIndentWithTabs() throws IOException { - new JsonArray().add(23).add(42).writeTo(output, indentWithTabs()); + @Test + public void testIndentWithTabs() throws IOException { + new JsonArray().add(23).add(42).writeTo(output, indentWithTabs()); - assertEquals("[\n\t23,\n\t42\n]", output.toString()); - } + assertEquals("[\n\t23,\n\t42\n]", output.toString()); + } - @Test - public void testIndentWithTabs_createsIndependentInstances() { - Writer writer = mock(Writer.class); + @Test + public void testIndentWithTabs_createsIndependentInstances() { + Writer writer = mock(Writer.class); - WriterConfig config = indentWithTabs(); - Object instance1 = config.createWriter(writer); - Object instance2 = config.createWriter(writer); + WriterConfig config = indentWithTabs(); + Object instance1 = config.createWriter(writer); + Object instance2 = config.createWriter(writer); - assertNotSame(instance1, instance2); - } + assertNotSame(instance1, instance2); + } - @Test - public void testSingleLine_nestedArray() throws IOException { - new JsonArray().add(23).add(new JsonArray().add(42)).writeTo(output, singleLine()); + @Test + public void testSingleLine_nestedArray() throws IOException { + new JsonArray().add(23).add(new JsonArray().add(42)).writeTo(output, singleLine()); - assertEquals("[23, [42]]", output.toString()); - } + assertEquals("[23, [42]]", output.toString()); + } - @Test - public void testSingleLine_nestedObject() throws IOException { - new JsonObject().add("a", 23) - .add("b", new JsonObject().add("c", 42)) - .writeTo(output, singleLine()); + @Test + public void testSingleLine_nestedObject() throws IOException { + new JsonObject().add("a", 23) + .add("b", new JsonObject().add("c", 42)) + .writeTo(output, singleLine()); - assertEquals("{\"a\": 23, \"b\": {\"c\": 42}}", output.toString()); - } + assertEquals("{\"a\": 23, \"b\": {\"c\": 42}}", output.toString()); + } - @Test - public void testSingleLine_createsIndependentInstances() { - Writer writer = mock(Writer.class); + @Test + public void testSingleLine_createsIndependentInstances() { + Writer writer = mock(Writer.class); - WriterConfig config = singleLine(); - Object instance1 = config.createWriter(writer); - Object instance2 = config.createWriter(writer); + WriterConfig config = singleLine(); + Object instance1 = config.createWriter(writer); + Object instance2 = config.createWriter(writer); - assertNotSame(instance1, instance2); - } + assertNotSame(instance1, instance2); + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/TestUtil.java b/src/test/java/io/github/jopenlibs/vault/json/TestUtil.java index 0cc8ccd2..a88dda88 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/TestUtil.java +++ b/src/test/java/io/github/jopenlibs/vault/json/TestUtil.java @@ -31,70 +31,75 @@ import static org.junit.Assert.assertNotNull; -@SuppressWarnings("PMD") -class TestUtil { - - static T assertException(Class type, - String message, - Runnable runnable) { - return assertException(type, message, adapt(runnable)); - } - - static T assertException(Class type, - String message, - RunnableEx runnable) { - T exception = assertException(type, runnable); - assertEquals("exception message", message, exception.getMessage()); - return exception; - } - - static T assertException(Class type, Runnable runnable) { - return assertException(type, adapt(runnable)); - } - - static T assertException(Class type, RunnableEx runnable) { - T exception = catchException(runnable, type); - assertNotNull("Expected exception: " + type.getName(), exception); - return exception; - } - - @SuppressWarnings("unchecked") - private static T catchException(RunnableEx runnable, Class type) { - try { - runnable.run(); - return null; - } catch (Exception exception) { - if (type.isAssignableFrom(exception.getClass())) { - return (T) exception; - } - String message = "Unexpected exception: " + exception.getMessage(); - throw new RuntimeException(message, exception); - } - } - - @SuppressWarnings("unchecked") - static T serializeAndDeserialize(T instance) throws Exception { - return (T) deserialize(serialize(instance)); - } - - private static byte[] serialize(Object object) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - new ObjectOutputStream(outputStream).writeObject(object); - return outputStream.toByteArray(); - } - - private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); - return new ObjectInputStream(inputStream).readObject(); - } - - private static RunnableEx adapt(final Runnable runnable) { - return runnable::run; - } - - public interface RunnableEx { - - void run() throws Exception; +public class TestUtil { + + public static T assertException(Class type, + String message, + Runnable runnable) + { + return assertException(type, message, adapt(runnable)); + } + + public static T assertException(Class type, + String message, + RunnableEx runnable) + + { + T exception = assertException(type, runnable); + assertEquals("exception message", message, exception.getMessage()); + return exception; + } + + public static T assertException(Class type, Runnable runnable) { + return assertException(type, adapt(runnable)); + } + + public static T assertException(Class type, RunnableEx runnable) { + T exception = catchException(runnable, type); + assertNotNull("Expected exception: " + type.getName(), exception); + return exception; + } + + @SuppressWarnings("unchecked") + private static T catchException(RunnableEx runnable, Class type) { + try { + runnable.run(); + return null; + } catch (Exception exception) { + if (type.isAssignableFrom(exception.getClass())) { + return (T)exception; + } + String message = "Unexpected exception: " + exception.getMessage(); + throw new RuntimeException(message, exception); } + } + + @SuppressWarnings("unchecked") + public static T serializeAndDeserialize(T instance) throws Exception { + return (T)deserialize(serialize(instance)); + } + + public static byte[] serialize(Object object) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(outputStream).writeObject(object); + return outputStream.toByteArray(); + } + + public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + return new ObjectInputStream(inputStream).readObject(); + } + + private static RunnableEx adapt(final Runnable runnable) { + return new RunnableEx() { + public void run() { + runnable.run(); + } + }; + } + + public static interface RunnableEx { + void run() throws Exception; + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/WritingBuffer_Test.java b/src/test/java/io/github/jopenlibs/vault/json/WritingBuffer_Test.java index 764348ba..3b3f617e 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/WritingBuffer_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/WritingBuffer_Test.java @@ -30,128 +30,127 @@ import static org.junit.Assert.assertEquals; -@SuppressWarnings("PMD") public class WritingBuffer_Test { - private static final int BUFFER_SIZE = 16; - private StringWriter wrapped; - private WritingBuffer writer; - - @Before - public void setUp() { - wrapped = new StringWriter(); - writer = new WritingBuffer(wrapped, BUFFER_SIZE); - } - - @Test - public void testFlushEmpty() throws IOException { - writer.flush(); - - assertEquals("", wrapped.toString()); - } - - @Test - public void testWriteChar() throws IOException { - writer.write('c'); - writer.flush(); - - assertEquals("c", wrapped.toString()); - } - - @Test - public void testWriteChar_fit() throws IOException { - writer.write(createString(BUFFER_SIZE - 1)); - writer.write('c'); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE - 1) + "c", wrapped.toString()); - } - - @Test - public void testWriteChar_exceeding() throws IOException { - writer.write(createString(BUFFER_SIZE)); - writer.write('c'); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE) + "c", wrapped.toString()); - } - - @Test - public void testWriteCharArray() throws IOException { - writer.write("foobar".toCharArray(), 1, 3); - writer.flush(); - - assertEquals("oob", wrapped.toString()); - } - - @Test - public void testWriteCharArray_fit() throws IOException { - writer.write(createString(BUFFER_SIZE - 3)); - writer.write("foobar".toCharArray(), 1, 3); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE - 3) + "oob", wrapped.toString()); - } - - @Test - public void testWriteCharArray_exceeding() throws IOException { - writer.write(createString(BUFFER_SIZE - 2)); - writer.write("foobar".toCharArray(), 1, 3); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE - 2) + "oob", wrapped.toString()); - } - - @Test - public void testWriteCharArray_exceedingBuffer() throws IOException { - writer.write(createChars(BUFFER_SIZE + 1)); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE + 1), wrapped.toString()); - } - - @Test - public void testWriteString() throws IOException { - writer.write("foobar", 1, 3); - writer.flush(); - - assertEquals("oob", wrapped.toString()); - } - - @Test - public void testWriteString_fit() throws IOException { - writer.write(createString(BUFFER_SIZE - 3)); - writer.write("foobar", 1, 3); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE - 3) + "oob", wrapped.toString()); - } - - @Test - public void testWriteString_exceeding() throws IOException { - writer.write(createString(BUFFER_SIZE - 2)); - writer.write("foobar", 1, 3); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE - 2) + "oob", wrapped.toString()); - } - - @Test - public void testWriteString_exceedingBuffer() throws IOException { - writer.write(createString(BUFFER_SIZE + 1)); - writer.flush(); - - assertEquals(createString(BUFFER_SIZE + 1), wrapped.toString()); - } - - private static String createString(int length) { - return new String(createChars(length)); - } - - private static char[] createChars(int length) { - char[] array = new char[length]; - Arrays.fill(array, 'x'); - return array; - } + private static final int BUFFER_SIZE = 16; + private StringWriter wrapped; + private WritingBuffer writer; + + @Before + public void setUp() { + wrapped = new StringWriter(); + writer = new WritingBuffer(wrapped, BUFFER_SIZE); + } + + @Test + public void testFlushEmpty() throws IOException { + writer.flush(); + + assertEquals("", wrapped.toString()); + } + + @Test + public void testWriteChar() throws IOException { + writer.write('c'); + writer.flush(); + + assertEquals("c", wrapped.toString()); + } + + @Test + public void testWriteChar_fit() throws IOException { + writer.write(createString(BUFFER_SIZE - 1)); + writer.write('c'); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE - 1) + "c", wrapped.toString()); + } + + @Test + public void testWriteChar_exceeding() throws IOException { + writer.write(createString(BUFFER_SIZE)); + writer.write('c'); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE) + "c", wrapped.toString()); + } + + @Test + public void testWriteCharArray() throws IOException { + writer.write("foobar".toCharArray(), 1, 3); + writer.flush(); + + assertEquals("oob", wrapped.toString()); + } + + @Test + public void testWriteCharArray_fit() throws IOException { + writer.write(createString(BUFFER_SIZE - 3)); + writer.write("foobar".toCharArray(), 1, 3); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE - 3) + "oob", wrapped.toString()); + } + + @Test + public void testWriteCharArray_exceeding() throws IOException { + writer.write(createString(BUFFER_SIZE - 2)); + writer.write("foobar".toCharArray(), 1, 3); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE - 2) + "oob", wrapped.toString()); + } + + @Test + public void testWriteCharArray_exceedingBuffer() throws IOException { + writer.write(createChars(BUFFER_SIZE + 1)); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE + 1), wrapped.toString()); + } + + @Test + public void testWriteString() throws IOException { + writer.write("foobar", 1, 3); + writer.flush(); + + assertEquals("oob", wrapped.toString()); + } + + @Test + public void testWriteString_fit() throws IOException { + writer.write(createString(BUFFER_SIZE - 3)); + writer.write("foobar", 1, 3); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE - 3) + "oob", wrapped.toString()); + } + + @Test + public void testWriteString_exceeding() throws IOException { + writer.write(createString(BUFFER_SIZE - 2)); + writer.write("foobar", 1, 3); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE - 2) + "oob", wrapped.toString()); + } + + @Test + public void testWriteString_exceedingBuffer() throws IOException { + writer.write(createString(BUFFER_SIZE + 1)); + writer.flush(); + + assertEquals(createString(BUFFER_SIZE + 1), wrapped.toString()); + } + + private static String createString(int length) { + return new String(createChars(length)); + } + + private static char[] createChars(int length) { + char[] array = new char[length]; + Arrays.fill(array, 'x'); + return array; + } } diff --git a/src/test/java/io/github/jopenlibs/vault/json/test/mocking/Mocking_Test.java b/src/test/java/io/github/jopenlibs/vault/json/test/mocking/Mocking_Test.java index 739acf4f..73fd133c 100644 --- a/src/test/java/io/github/jopenlibs/vault/json/test/mocking/Mocking_Test.java +++ b/src/test/java/io/github/jopenlibs/vault/json/test/mocking/Mocking_Test.java @@ -34,33 +34,32 @@ /** * Make sure types do not prevent mocking by final or visibility constructs. */ -@SuppressWarnings("PMD") public class Mocking_Test { - @Test - public void mockValue() { - JsonValue jsonValue = Mockito.mock(JsonValue.class); + @Test + public void mockValue() { + JsonValue jsonValue = Mockito.mock(JsonValue.class); - assertNotNull(jsonValue); - } + assertNotNull(jsonValue); + } - @Test - public void mockObject() { - JsonObject jsonObject = Mockito.mock(JsonObject.class); + @Test + public void mockObject() { + JsonObject jsonObject = Mockito.mock(JsonObject.class); - assertNotNull(jsonObject); - } + assertNotNull(jsonObject); + } - @Test - public void mockArray() { - JsonArray jsonArray = Mockito.mock(JsonArray.class); + @Test + public void mockArray() { + JsonArray jsonArray = Mockito.mock(JsonArray.class); - assertNotNull(jsonArray); - } + assertNotNull(jsonArray); + } - @Test - public void mockParseException() { - Mockito.mock(ParseException.class); - } + @Test + public void mockParseException() { + Mockito.mock(ParseException.class); + } } From adac3f2c704b222508d81cf76e3d56350c55872a Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 00:52:33 +0200 Subject: [PATCH 11/18] Added class to manage maximum nesting depth --- .../vault/internal/JsonNestingException.java | 14 ++ .../jopenlibs/vault/internal/SafeJson.java | 78 +++++++++++ .../vault/internal/SafeJsonTests.java | 128 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/main/java/io/github/jopenlibs/vault/internal/JsonNestingException.java create mode 100644 src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java create mode 100644 src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java diff --git a/src/main/java/io/github/jopenlibs/vault/internal/JsonNestingException.java b/src/main/java/io/github/jopenlibs/vault/internal/JsonNestingException.java new file mode 100644 index 00000000..210bf245 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/internal/JsonNestingException.java @@ -0,0 +1,14 @@ +package io.github.jopenlibs.vault.internal; + +/** + * Thrown by {@link SafeJson#parse(String)} when JSON is nested deeper than + * {@link SafeJson#MAX_NESTING_DEPTH}. + */ +public class JsonNestingException extends RuntimeException { + + private static final long serialVersionUID = 6370532845419260815L; + + public JsonNestingException(final String message) { + super(message); + } +} diff --git a/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java b/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java new file mode 100644 index 00000000..9fa2e0d2 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java @@ -0,0 +1,78 @@ +package io.github.jopenlibs.vault.internal; + +import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.json.JsonValue; + +/** + * Parses JSON received from Vault, rejecting documents nested too deeply before they reach the + * vendored parser. + * + *

The vendored parser descends into nested arrays and objects by recursion. Its own nesting + * limit (1000 levels) needs more stack than some threads have, so a deeply nested value stored in + * Vault could still raise {@link StackOverflowError} in the reading thread. This class checks the + * nesting depth first, with a loop that uses no stack per level, and throws + * {@link JsonNestingException} above {@link #MAX_NESTING_DEPTH}.

+ * + *

This class is internal to the driver: its package is not exported by the module.

+ */ +public final class SafeJson { + + /** + * Maximum depth of nested arrays and objects accepted. Vault 1.20 and later reject JSON nested + * deeper than 298 levels, so valid Vault responses stay well below this limit. + */ + public static final int MAX_NESTING_DEPTH = 500; + + private SafeJson() { + } + + /** + * Parses the given string as JSON, after checking its nesting depth. + * + * @param string the input string, must be valid JSON + * @return a value that represents the parsed JSON + * @throws JsonNestingException if arrays and objects are nested deeper than + * {@link #MAX_NESTING_DEPTH} + * @throws io.github.jopenlibs.vault.json.ParseException if the input is not valid JSON + */ + public static JsonValue parse(final String string) { + checkNestingDepth(string, MAX_NESTING_DEPTH); + return Json.parse(string); + } + + /** + * Checks that arrays and objects in the given JSON text are not nested deeper than the given + * limit. Brackets inside strings are ignored. Other syntax errors are left to the parser. + * + * @param string the JSON text, ignored if null + * @param maxDepth the maximum nesting depth accepted + * @throws JsonNestingException if the limit is exceeded + */ + static void checkNestingDepth(final String string, final int maxDepth) { + if (string == null) { + return; + } + + int depth = 0; + boolean inString = false; + for (int i = 0; i < string.length(); i++) { + final char c = string.charAt(i); + if (inString) { + if (c == '\\') { + i++; + } else if (c == '"') { + inString = false; + } + } else if (c == '"') { + inString = true; + } else if (c == '[' || c == '{') { + if (++depth > maxDepth) { + throw new JsonNestingException(String.format( + "JSON nested deeper than %d levels at offset %d", maxDepth, i)); + } + } else if ((c == ']' || c == '}') && depth > 0) { + depth--; + } + } + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java b/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java new file mode 100644 index 00000000..95b8396d --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java @@ -0,0 +1,128 @@ +package io.github.jopenlibs.vault.internal; + +import io.github.jopenlibs.vault.api.Logical.logicalOperations; +import io.github.jopenlibs.vault.response.AuthResponse; +import io.github.jopenlibs.vault.response.LogicalResponse; +import io.github.jopenlibs.vault.rest.RestResponse; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class SafeJsonTests { + + /** Depth of the payload in the vulnerability report, accepted by Vault before 1.20. */ + private static final int REPORTED_DEPTH = 9998; + + private static String nestedArrays(final int depth) { + return "[".repeat(depth) + "]".repeat(depth); + } + + private static String nestedObjects(final int depth) { + return "{\"a\":".repeat(depth) + "1" + "}".repeat(depth); + } + + @Test + public void testArraysAtLimitAccepted() { + final var value = SafeJson.parse(nestedArrays(SafeJson.MAX_NESTING_DEPTH)); + assertTrue(value.isArray()); + } + + @Test + public void testObjectsAtLimitAccepted() { + final var value = SafeJson.parse(nestedObjects(SafeJson.MAX_NESTING_DEPTH)); + assertTrue(value.isObject()); + } + + @Test + public void testArraysAboveLimitRejected() { + final var input = nestedArrays(SafeJson.MAX_NESTING_DEPTH + 1); + final var e = assertThrows(JsonNestingException.class, () -> SafeJson.parse(input)); + assertEquals("JSON nested deeper than 500 levels at offset 500", e.getMessage()); + } + + @Test + public void testObjectsAboveLimitRejected() { + final var input = nestedObjects(SafeJson.MAX_NESTING_DEPTH + 1); + assertThrows(JsonNestingException.class, () -> SafeJson.parse(input)); + } + + @Test + public void testMixedNestingRejected() { + final var input = "{\"a\":[".repeat(SafeJson.MAX_NESTING_DEPTH) + "]}".repeat( + SafeJson.MAX_NESTING_DEPTH); + assertThrows(JsonNestingException.class, () -> SafeJson.parse(input)); + } + + @Test + public void testDepthIsNotCumulative() { + final var sibling = nestedArrays(SafeJson.MAX_NESTING_DEPTH - 1); + final var input = "[" + sibling + "," + sibling + "," + sibling + "]"; + assertTrue(SafeJson.parse(input).isArray()); + } + + @Test + public void testBracketsInStringsIgnored() { + final var input = "{\"key\":\"" + "[{".repeat(SafeJson.MAX_NESTING_DEPTH * 2) + "\"}"; + assertEquals("[{".repeat(SafeJson.MAX_NESTING_DEPTH * 2), + SafeJson.parse(input).asObject().getString("key", null)); + } + + @Test + public void testEscapedQuotesInStrings() { + // The escaped quote does not end the string, so the brackets after it are still ignored + final var input = "[\"\\\"" + "[".repeat(SafeJson.MAX_NESTING_DEPTH + 1) + "\"]"; + assertEquals("\"" + "[".repeat(SafeJson.MAX_NESTING_DEPTH + 1), + SafeJson.parse(input).asArray().get(0).asString()); + } + + @Test + public void testEscapedBackslashEndsString() { + // "\\" is a complete string, so the brackets after it count + final var input = "[\"\\\\\"," + nestedArrays(SafeJson.MAX_NESTING_DEPTH) + "]"; + assertThrows(JsonNestingException.class, () -> SafeJson.parse(input)); + } + + @Test + public void testNullLeftToParser() { + assertThrows(NullPointerException.class, () -> SafeJson.parse(null)); + } + + @Test + public void testDeeplyNestedAuthResponseIgnored() { + final var body = "{\"auth\":{\"client_token\":\"t\",\"metadata\":" + + nestedArrays(REPORTED_DEPTH) + "}}"; + final var response = new AuthResponse( + new RestResponse(200, "application/json", body.getBytes(StandardCharsets.UTF_8)), + 0); + assertNull(response.getAuthClientToken()); + } + + /** + * The scenario of the vulnerability report: a deeply nested value read through + * {@code Logical.read()} must not kill the reading thread, even with a small stack. + */ + @Test + public void testDeeplyNestedLogicalResponseOnSmallStack() throws InterruptedException { + final var body = "{\"data\":{\"data\":{\"note\":\"looks-normal\",\"payload\":" + + nestedArrays(REPORTED_DEPTH) + "}}}"; + final var restResponse = new RestResponse(200, "application/json", + body.getBytes(StandardCharsets.UTF_8)); + final var result = new AtomicReference(); + final var error = new AtomicReference(); + + final var thread = new Thread(null, + () -> result.set(new LogicalResponse(restResponse, 0, logicalOperations.readV2)), + "small-stack-reader", 256 * 1024); + thread.setUncaughtExceptionHandler((t, e) -> error.set(e)); + thread.start(); + thread.join(); + + assertNull(error.get()); + assertTrue(result.get().getData().isEmpty()); + } +} From d75a1fde22de41f31a72a6c8d2a8c63f3896b02f Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 00:53:02 +0200 Subject: [PATCH 12/18] Updated methods to use new safe JSON class --- README.md | 4 ++++ src/main/java/io/github/jopenlibs/vault/VaultImpl.java | 4 ++-- .../github/jopenlibs/vault/response/AuthResponse.java | 7 ++++--- .../jopenlibs/vault/response/HealthResponse.java | 4 ++-- .../jopenlibs/vault/response/LogicalResponse.java | 10 +++++----- .../jopenlibs/vault/response/LookupResponse.java | 7 ++++--- .../github/jopenlibs/vault/response/SealResponse.java | 7 ++++--- .../github/jopenlibs/vault/response/WrapResponse.java | 7 ++++--- 8 files changed, 29 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c0ff308e..d809682d 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,10 @@ patches. * Security: fixes [CVE-2026-109408](https://www.cve.org/CVERecord?id=CVE-2026-109408), where SSL certificate verification could be silently disabled. See the `VAULT_SSL_VERIFY` and `SslConfig` entries below + * Security: a deeply nested JSON value stored in Vault no longer kills the reading thread with an + uncaught `StackOverflowError`. Responses nested deeper than 500 levels are now ignored like + other malformed responses (Vault 1.20 and later reject values nested deeper than 298 levels). + The vendored JSON parser also rejects documents nested deeper than 1000 levels * Environment variables: surrounding whitespace is removed, and empty values are treated as unset * `VAULT_SSL_VERIFY` now only disables SSL verification when set to `false`. Previously any value other than `true` disabled it, including `1`, `yes`, typos and empty values. **Values like `0`, diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 64bce224..16fe5a3a 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -9,7 +9,7 @@ import io.github.jopenlibs.vault.api.sys.Seal; import io.github.jopenlibs.vault.api.sys.Sys; import io.github.jopenlibs.vault.api.sys.mounts.Mounts; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.json.JsonValue; import io.github.jopenlibs.vault.rest.Rest; import io.github.jopenlibs.vault.rest.RestException; @@ -375,7 +375,7 @@ private Map collectSecretEngineVersions() { final var jsonString = new String(restResponse.getBody(), StandardCharsets.UTF_8); final var data = new HashMap(); - final var jsonData = Json.parse(jsonString).asObject().get("data").asObject(); + final var jsonData = SafeJson.parse(jsonString).asObject().get("data").asObject(); for (final var member : jsonData) { final String name = member.getName(); String version = "unknown"; diff --git a/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java b/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java index 354dc377..61ebf97b 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java @@ -1,6 +1,7 @@ package io.github.jopenlibs.vault.response; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.JsonNestingException; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.json.JsonObject; import io.github.jopenlibs.vault.json.JsonValue; import io.github.jopenlibs.vault.json.ParseException; @@ -38,7 +39,7 @@ public AuthResponse(final RestResponse restResponse, final int retries) { try { final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); - jsonResponse = Json.parse(responseJson).asObject(); + jsonResponse = SafeJson.parse(responseJson).asObject(); final var authJsonVal = jsonResponse.get("auth"); final var authJsonObject = hasValue(authJsonVal) ? authJsonVal.asObject() : null; @@ -69,7 +70,7 @@ public AuthResponse(final RestResponse restResponse, final int retries) { if (hasValue(renewableJsonVal)) { renewable = renewableJsonVal.asBoolean(); } - } catch (ParseException e) { + } catch (ParseException | JsonNestingException e) { // No-op. } } diff --git a/src/main/java/io/github/jopenlibs/vault/response/HealthResponse.java b/src/main/java/io/github/jopenlibs/vault/response/HealthResponse.java index efba17fd..6b7954d3 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/HealthResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/HealthResponse.java @@ -2,7 +2,7 @@ import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.Debug; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.rest.RestResponse; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -59,7 +59,7 @@ public HealthResponse(final RestResponse restResponse, final int retries) try { final var jsonString = new String(restResponse.getBody(), StandardCharsets.UTF_8);//NOPMD - final var jsonObject = Json.parse(jsonString).asObject(); + final var jsonObject = SafeJson.parse(jsonString).asObject(); this.initialized = jsonObject.get("initialized") == null ? null : jsonObject.get("initialized").asBoolean(); this.sealed = jsonObject.get("sealed") == null ? null diff --git a/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java b/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java index de85698d..9feb362f 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java @@ -2,7 +2,7 @@ import io.github.jopenlibs.vault.api.Logical; import io.github.jopenlibs.vault.api.Logical.logicalOperations; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.json.JsonObject; import io.github.jopenlibs.vault.json.JsonValue; import io.github.jopenlibs.vault.rest.RestResponse; @@ -80,7 +80,7 @@ private void parseMetadataFields() { try { final var jsonString = new String(getRestResponse().getBody(), StandardCharsets.UTF_8); - final var jsonObject = Json.parse(jsonString).asObject(); + final var jsonObject = SafeJson.parse(jsonString).asObject(); this.leaseId = jsonObject.get("lease_id").asString(); this.renewable = jsonObject.get("renewable").asBoolean(); @@ -95,7 +95,7 @@ private void parseResponseData(final Logical.logicalOperations operation) { try { final var jsonString = new String(getRestResponse().getBody(), StandardCharsets.UTF_8); - var jsonObject = Json.parse(jsonString).asObject(); + var jsonObject = SafeJson.parse(jsonString).asObject(); if (operation.equals(Logical.logicalOperations.readV2)) { jsonObject = jsonObject.get("data").asObject(); final var metadataValue = jsonObject.get("metadata"); @@ -112,7 +112,7 @@ private void parseResponseData(final Logical.logicalOperations operation) { Logical.logicalOperations.listV2)) { if (getRestResponse().getStatus() != 404 && data.get("keys") != null) { - final var keys = Json.parse(data.get("keys")).asArray(); + final var keys = SafeJson.parse(data.get("keys")).asArray(); keys.forEach(key -> listData.add(key.asString())); } @@ -120,7 +120,7 @@ private void parseResponseData(final Logical.logicalOperations operation) { if (operation.equals(logicalOperations.listSubKeys)) { if (data.containsKey("subkeys")) { - final var keys = Json.parse(data.get("subkeys")).asObject(); + final var keys = SafeJson.parse(data.get("subkeys")).asObject(); this.listSubkeys.addAll(keys.names()); } } diff --git a/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java b/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java index 8006d210..471b0655 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java @@ -1,6 +1,7 @@ package io.github.jopenlibs.vault.response; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.JsonNestingException; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.json.JsonValue; import io.github.jopenlibs.vault.json.ParseException; import io.github.jopenlibs.vault.rest.RestResponse; @@ -40,7 +41,7 @@ public LookupResponse(final RestResponse restResponse, final int retries) { try { final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); - final var jsonObject = Json.parse(responseJson).asObject(); + final var jsonObject = SafeJson.parse(responseJson).asObject(); final var dataJsonVal = jsonObject.get("data"); if (!hasValue(dataJsonVal)) { return; @@ -74,7 +75,7 @@ public LookupResponse(final RestResponse restResponse, final int retries) { renewable = dataJsonObject.getBoolean("renewable", false); ttl = dataJsonObject.getLong("ttl", 0); - } catch (ParseException ignored) { + } catch (ParseException | JsonNestingException ignored) { } } diff --git a/src/main/java/io/github/jopenlibs/vault/response/SealResponse.java b/src/main/java/io/github/jopenlibs/vault/response/SealResponse.java index 2908af11..4be33964 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/SealResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/SealResponse.java @@ -1,6 +1,7 @@ package io.github.jopenlibs.vault.response; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.JsonNestingException; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.json.ParseException; import io.github.jopenlibs.vault.rest.RestResponse; import java.nio.charset.StandardCharsets; @@ -27,14 +28,14 @@ public SealResponse(final RestResponse restResponse, final int retries) { try { final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); - final var jsonObject = Json.parse(responseJson).asObject(); + final var jsonObject = SafeJson.parse(responseJson).asObject(); sealed = jsonObject.getBoolean("sealed", false); threshold = jsonObject.getLong("t", 0); numberOfShares = jsonObject.getLong("n", 0); progress = jsonObject.getLong("progress", 0); - } catch (ParseException ignored) { + } catch (ParseException | JsonNestingException ignored) { } } diff --git a/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java b/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java index dcf5a06b..3decf62e 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java @@ -1,6 +1,7 @@ package io.github.jopenlibs.vault.response; -import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.internal.JsonNestingException; +import io.github.jopenlibs.vault.internal.SafeJson; import io.github.jopenlibs.vault.json.ParseException; import io.github.jopenlibs.vault.rest.RestResponse; import java.nio.charset.StandardCharsets; @@ -29,7 +30,7 @@ public WrapResponse(final RestResponse restResponse, final int retries) { try { final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); - final var jsonResponse = Json.parse(responseJson).asObject(); + final var jsonResponse = SafeJson.parse(responseJson).asObject(); final var wrapInfoJsonVal = jsonResponse.get("wrap_info"); if (hasValue(wrapInfoJsonVal)) { final var wrapInfoJsonObject = wrapInfoJsonVal.asObject(); @@ -45,7 +46,7 @@ public WrapResponse(final RestResponse restResponse, final int retries) { if (hasValue(renewableJsonVal)) { renewable = renewableJsonVal.asBoolean(); } - } catch (ParseException e) { + } catch (ParseException | JsonNestingException e) { // No-op. } } From 097d63c53afe9836db52d4f06fffda00f16d081a Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 13:01:14 +0200 Subject: [PATCH 13/18] Clarified scope of JSON package --- .../io/github/jopenlibs/vault/json/package-info.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/io/github/jopenlibs/vault/json/package-info.java b/src/main/java/io/github/jopenlibs/vault/json/package-info.java index e1a11ec9..8e7645ad 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/package-info.java +++ b/src/main/java/io/github/jopenlibs/vault/json/package-info.java @@ -6,5 +6,17 @@ * under the MIT License. The package names have all been changed, to prevent any conflicts should * you happen to be using a different version of this library elsewhere in your project * dependencies.

+ * + *

This package is not part of the supported API of the driver. It is exported only + * because some public methods take or return its types, such as + * {@link io.github.jopenlibs.vault.response.LogicalResponse#getDataObject()}, + * {@link io.github.jopenlibs.vault.response.UnwrapResponse#getData()}, + * {@link io.github.jopenlibs.vault.api.Auth#wrap(JsonObject, int)}, + * {@link io.github.jopenlibs.vault.api.sys.Wrapping#wrap(JsonObject, int)} and + * {@link io.github.jopenlibs.vault.api.sys.mounts.MountPayload}. Use {@link Json}, + * {@link JsonObject}, {@link JsonArray}, {@link JsonValue} and {@link ParseException} only as far + * as those methods need. Other classes, such as {@link JsonParser} and {@link JsonHandler}, are + * deprecated for use outside the driver. Any class here may change in a minor release, and the + * package is expected to be removed from the public API in a future major release.

*/ package io.github.jopenlibs.vault.json; From daf0dae86c076e174babe680b648a134ce97ca55 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Fri, 16 Oct 2026 13:26:50 +0200 Subject: [PATCH 14/18] Fixed Javadoc in vendored library (needed to pass CI tests) --- src/main/java/io/github/jopenlibs/vault/json/JsonArray.java | 2 +- src/main/java/io/github/jopenlibs/vault/json/JsonObject.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java b/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java index fc812025..727e2ac9 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonArray.java @@ -490,7 +490,7 @@ public int hashCode() { * * @param object * the object to be compared with this JsonArray - * @return true if the specified object is equal to this JsonArray, false + * @return true if the specified object is equal to this JsonArray, false * otherwise */ @Override diff --git a/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java b/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java index c8c95a28..8a5b97f9 100644 --- a/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java +++ b/src/main/java/io/github/jopenlibs/vault/json/JsonObject.java @@ -867,7 +867,7 @@ public int hashCode() { * * @param object * the object to be compared with this JsonObject - * @return true if the specified object is equal to this JsonObject, false + * @return true if the specified object is equal to this JsonObject, false * otherwise */ @Override From 19abeb80326ad1dfa01f9610143ac8d568d78c34 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 21 Oct 2026 23:11:54 +0200 Subject: [PATCH 15/18] Lowered nesting depth to mitigate StackOverflowError errors --- README.md | 2 +- .../java/io/github/jopenlibs/vault/internal/SafeJson.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d809682d..f77fb133 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ patches. certificate verification could be silently disabled. See the `VAULT_SSL_VERIFY` and `SslConfig` entries below * Security: a deeply nested JSON value stored in Vault no longer kills the reading thread with an - uncaught `StackOverflowError`. Responses nested deeper than 500 levels are now ignored like + uncaught `StackOverflowError`. Responses nested deeper than 320 levels are now ignored like other malformed responses (Vault 1.20 and later reject values nested deeper than 298 levels). The vendored JSON parser also rejects documents nested deeper than 1000 levels * Environment variables: surrounding whitespace is removed, and empty values are treated as unset diff --git a/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java b/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java index 9fa2e0d2..dd64d825 100644 --- a/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java +++ b/src/main/java/io/github/jopenlibs/vault/internal/SafeJson.java @@ -19,9 +19,12 @@ public final class SafeJson { /** * Maximum depth of nested arrays and objects accepted. Vault 1.20 and later reject JSON nested - * deeper than 298 levels, so valid Vault responses stay well below this limit. + * deeper than 298 levels. The limit applies to the whole response, so it leaves a margin for + * the levels Vault wraps around a stored value (such as data.data in KV version + * 2). It is kept this low because the parser recurses: every level uses stack, and on threads + * with a small stack a higher limit could still let a value raise {@link StackOverflowError}. */ - public static final int MAX_NESTING_DEPTH = 500; + public static final int MAX_NESTING_DEPTH = 320; private SafeJson() { } From 6444267ea42ae8fde92edbf6b5b70d322c1309b4 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 21 Oct 2026 23:12:21 +0200 Subject: [PATCH 16/18] Added tests --- .../vault/internal/SafeJsonTests.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java b/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java index 95b8396d..f0e749e4 100644 --- a/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java +++ b/src/test/java/io/github/jopenlibs/vault/internal/SafeJsonTests.java @@ -42,7 +42,7 @@ public void testObjectsAtLimitAccepted() { public void testArraysAboveLimitRejected() { final var input = nestedArrays(SafeJson.MAX_NESTING_DEPTH + 1); final var e = assertThrows(JsonNestingException.class, () -> SafeJson.parse(input)); - assertEquals("JSON nested deeper than 500 levels at offset 500", e.getMessage()); + assertEquals("JSON nested deeper than 320 levels at offset 320", e.getMessage()); } @Test @@ -108,8 +108,28 @@ public void testDeeplyNestedAuthResponseIgnored() { */ @Test public void testDeeplyNestedLogicalResponseOnSmallStack() throws InterruptedException { + assertTrue(readOnSmallStack(REPORTED_DEPTH).getData().isEmpty()); + } + + /** + * A response nested exactly at the limit passes the depth check and reaches the recursive + * parser, so it must fit in a small stack. + */ + @Test + public void testLogicalResponseAtLimitOnSmallStack() throws InterruptedException { + final var data = readOnSmallStack(SafeJson.MAX_NESTING_DEPTH - 3).getData(); + assertEquals("looks-normal", data.get("note")); + } + + /** + * Reads a KV version 2 response through {@link LogicalResponse} in a thread with a 256 KB + * stack, failing if the thread dies. The payload is nested under three levels of the response + * (data.data.payload). + */ + private static LogicalResponse readOnSmallStack(final int payloadDepth) + throws InterruptedException { final var body = "{\"data\":{\"data\":{\"note\":\"looks-normal\",\"payload\":" - + nestedArrays(REPORTED_DEPTH) + "}}}"; + + nestedArrays(payloadDepth) + "}}}"; final var restResponse = new RestResponse(200, "application/json", body.getBytes(StandardCharsets.UTF_8)); final var result = new AtomicReference(); @@ -123,6 +143,6 @@ public void testDeeplyNestedLogicalResponseOnSmallStack() throws InterruptedExce thread.join(); assertNull(error.get()); - assertTrue(result.get().getData().isEmpty()); + return result.get(); } } From 227b5a6c3e2b71ff5cdb8d03eef40673d0fab74c Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 21 Oct 2026 23:13:01 +0200 Subject: [PATCH 17/18] Documented thread stack size --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f77fb133..e9b19668 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Table of Contents * [Java Keystore (JKS) based config](#java-keystore-jks-based-config) * [OpenSSL (PEM) based config](#openssl-pem-based-config) * [Using the driver](#using-the-driver) + * [Thread stack size](#thread-stack-size) * [API Reference (Javadocs)](#api-reference-javadocs) * [Version History](#version-history) * [Development](#development) @@ -355,6 +356,16 @@ Only transient failures are retried: I/O errors (e.g. connection refused or time 408, 412 and 429 responses. Other 4xx responses (e.g. 400, 403, 404) fail immediately, since retrying them would not help. +#### Thread stack size + +The driver parses JSON responses recursively, so every level of nested arrays and objects uses +some of the calling thread's stack. Responses nested deeper than 320 levels are ignored before +parsing, like other malformed responses. To parse a response up to that limit, call the driver +from threads with a stack of at least 256 KB. The default JVM thread stack (usually 1 MB) is +enough. Only threads created with a smaller stack size, for example through +`new Thread(group, task, name, stackSize)` or the `-Xss` option, can be affected: on those, a +deeply nested value stored in Vault can raise `StackOverflowError` in the reading thread. + API Reference (Javadocs) ------------------------ Full [Javadoc documentation](https://jopenlibs.github.io/vault-java-driver/javadoc). @@ -374,7 +385,8 @@ patches. * Security: a deeply nested JSON value stored in Vault no longer kills the reading thread with an uncaught `StackOverflowError`. Responses nested deeper than 320 levels are now ignored like other malformed responses (Vault 1.20 and later reject values nested deeper than 298 levels). - The vendored JSON parser also rejects documents nested deeper than 1000 levels + The vendored JSON parser also rejects documents nested deeper than 1000 levels. Threads calling + the driver need a stack of at least 256 KB: see [Thread stack size](#thread-stack-size) * Environment variables: surrounding whitespace is removed, and empty values are treated as unset * `VAULT_SSL_VERIFY` now only disables SSL verification when set to `false`. Previously any value other than `true` disabled it, including `1`, `yes`, typos and empty values. **Values like `0`, From b93553d49e1eb70cfff0fd721f945e4ec416bbbe Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 21 Oct 2026 23:13:59 +0200 Subject: [PATCH 18/18] Updated changelog --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e9b19668..915299e4 100644 --- a/README.md +++ b/README.md @@ -382,8 +382,9 @@ patches. * Security: fixes [CVE-2026-109408](https://www.cve.org/CVERecord?id=CVE-2026-109408), where SSL certificate verification could be silently disabled. See the `VAULT_SSL_VERIFY` and `SslConfig` entries below - * Security: a deeply nested JSON value stored in Vault no longer kills the reading thread with an - uncaught `StackOverflowError`. Responses nested deeper than 320 levels are now ignored like + * Security: fixes [CVE-2026-112072](https://www.cve.org/CVERecord?id=CVE-2026-112072), where a + deeply nested JSON value stored in Vault could kill the reading thread with an uncaught + `StackOverflowError`. Responses nested deeper than 320 levels are now ignored like other malformed responses (Vault 1.20 and later reject values nested deeper than 298 levels). The vendored JSON parser also rejects documents nested deeper than 1000 levels. Threads calling the driver need a stack of at least 256 KB: see [Thread stack size](#thread-stack-size)