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 diff --git a/README.md b/README.md index 47971243..d809682d 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. @@ -356,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`, @@ -407,6 +426,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 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..9682bc61 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/RetryPolicy.java @@ -0,0 +1,73 @@ +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.

+ * + * @see Vault#retrying(int, int) + * @see VaultConfig#retries(int, int) + */ +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/main/java/io/github/jopenlibs/vault/Vault.java b/src/main/java/io/github/jopenlibs/vault/Vault.java index dffb117f..e696d542 100644 --- a/src/main/java/io/github/jopenlibs/vault/Vault.java +++ b/src/main/java/io/github/jopenlibs/vault/Vault.java @@ -50,17 +50,58 @@ 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); + /** + *

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 + * @see VaultConfig#retries(int, int) + */ + 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/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index c480c487..190337d3 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -319,12 +319,38 @@ 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.

* *

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. @@ -338,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 04b81768..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; @@ -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); * @@ -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()) { @@ -149,22 +166,51 @@ 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); 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 +218,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 +229,7 @@ public Logical logical() { */ @Override public Auth auth() { - return new Auth(vaultConfig); + return new Auth(vaultConfig, retryPolicy); } /** @@ -194,7 +240,7 @@ public Auth auth() { */ @Override public Sys sys() { - return new Sys(vaultConfig); + return new Sys(vaultConfig, retryPolicy); } /** @@ -205,7 +251,7 @@ public Sys sys() { */ @Override public Pki pki() { - return new Pki(vaultConfig); + return new Pki(vaultConfig, "pki", retryPolicy); } /** @@ -232,17 +278,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 +301,7 @@ public Database database(final String mountPath) { @Override @Deprecated public Leases leases() { - return new Leases(vaultConfig); + return new Leases(vaultConfig, retryPolicy); } /** @@ -265,7 +311,7 @@ public Leases leases() { */ @Override public Debug debug() { - return new Debug(vaultConfig); + return new Debug(vaultConfig, retryPolicy); } /** @@ -279,7 +325,7 @@ public Debug debug() { @Override @Deprecated public Mounts mounts() { - return new Mounts(vaultConfig); + return new Mounts(vaultConfig, retryPolicy); } /** @@ -292,7 +338,7 @@ public Mounts mounts() { @Override @Deprecated public Seal seal() { - return new Seal(vaultConfig); + return new Seal(vaultConfig, retryPolicy); } /** @@ -329,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/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 ca7c91e7..9844a357 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(); } @@ -164,8 +175,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() ); } @@ -431,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())) @@ -488,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())) @@ -532,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/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/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/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/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/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/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..727e2ac9 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..8a5b97f9 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/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; 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/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/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. } } 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()); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/RetryTests.java b/src/test/java/io/github/jopenlibs/vault/RetryTests.java index 2b021f9b..cb47620d 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,213 @@ 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)); + } + + @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); + } + } + + @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(); + } + + 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); + } + } 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 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(); 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(); + } +} 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()); + } +} 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); + } }