From 4c67c1a7dbbd051a07285fd2dc15f5e65b462b4f Mon Sep 17 00:00:00 2001 From: Scott Fauerbach <scottfauerbach@gmail.com> Date: Thu, 1 Feb 2024 11:43:40 -0500 Subject: [PATCH] Revert "Extract json, nkey and jwt utils to libraries (#1061)" (#1071) This reverts commit 7b1b22366a5429afe8034bbffc71a992fa966ecd. --- build.gradle | 5 +- .../jetstream/NatsJsMirrorSubUseCases.java | 2 +- src/main/java/io/nats/client/NKey.java | 741 ++++++++++++++ .../io/nats/client/PullRequestOptions.java | 16 +- .../java/io/nats/client/PurgeOptions.java | 10 +- .../java/io/nats/client/api/ApiResponse.java | 10 +- .../client/api/ConsumerConfiguration.java | 62 +- .../client/api/ConsumerCreateRequest.java | 5 +- .../io/nats/client/api/ConsumerLimits.java | 8 +- .../java/io/nats/client/api/External.java | 2 +- .../nats/client/api/MessageDeleteRequest.java | 2 +- .../io/nats/client/api/MessageGetRequest.java | 2 +- .../java/io/nats/client/api/MessageInfo.java | 30 +- .../java/io/nats/client/api/ObjectInfo.java | 15 +- .../java/io/nats/client/api/ObjectLink.java | 8 +- .../java/io/nats/client/api/ObjectMeta.java | 15 +- .../io/nats/client/api/ObjectMetaOptions.java | 8 +- .../java/io/nats/client/api/Placement.java | 2 +- .../java/io/nats/client/api/Republish.java | 2 +- .../java/io/nats/client/api/SourceBase.java | 18 +- .../nats/client/api/StreamConfiguration.java | 14 +- .../io/nats/client/api/StreamInfoOptions.java | 2 +- .../io/nats/client/api/SubjectTransform.java | 2 +- .../io/nats/client/impl/StreamInfoReader.java | 2 +- .../io/nats/client/support/DateTimeUtils.java | 80 ++ .../java/io/nats/client/support/Encoding.java | 265 +++++ .../io/nats/client/support/HeadersUtils.java | 37 - .../client/support/JsonParseException.java | 17 + .../io/nats/client/support/JsonParser.java | 439 ++++++++ .../nats/client/support/JsonSerializable.java | 28 + .../io/nats/client/support/JsonUtils.java | 315 ++++-- .../io/nats/client/support/JsonValue.java | 275 +++++ .../nats/client/support/JsonValueUtils.java | 375 +++++++ .../java/io/nats/client/support/JwtUtils.java | 171 ++-- .../io/nats/client/support/Validator.java | 30 - src/main/java/io/nats/service/Endpoint.java | 15 +- .../java/io/nats/service/EndpointStats.java | 26 +- .../java/io/nats/service/InfoResponse.java | 11 +- src/main/java/io/nats/service/Service.java | 13 +- .../java/io/nats/service/ServiceResponse.java | 18 +- .../java/io/nats/service/StatsResponse.java | 7 +- src/test/java/io/nats/client/AuthTests.java | 7 +- src/test/java/io/nats/client/NKeyTests.java | 600 +++++++++++ .../client/api/AccountStatisticsTests.java | 3 +- .../nats/client/impl/JetStreamPullTests.java | 12 +- .../nats/client/impl/ListRequestsTests.java | 3 +- .../client/support/DateTimeUtilsTests.java | 86 ++ .../io/nats/client/support/EncodingTests.java | 8 - .../nats/client/support/JsonParsingTests.java | 950 ++++++++++++++++++ .../io/nats/client/support/JwtUtilsTests.java | 3 - .../java/io/nats/service/ServiceTests.java | 15 +- 51 files changed, 4379 insertions(+), 413 deletions(-) create mode 100644 src/main/java/io/nats/client/NKey.java create mode 100644 src/main/java/io/nats/client/support/DateTimeUtils.java create mode 100644 src/main/java/io/nats/client/support/Encoding.java delete mode 100644 src/main/java/io/nats/client/support/HeadersUtils.java create mode 100644 src/main/java/io/nats/client/support/JsonParseException.java create mode 100644 src/main/java/io/nats/client/support/JsonParser.java create mode 100644 src/main/java/io/nats/client/support/JsonSerializable.java create mode 100644 src/main/java/io/nats/client/support/JsonValue.java create mode 100644 src/main/java/io/nats/client/support/JsonValueUtils.java create mode 100644 src/test/java/io/nats/client/NKeyTests.java create mode 100644 src/test/java/io/nats/client/support/DateTimeUtilsTests.java create mode 100644 src/test/java/io/nats/client/support/JsonParsingTests.java diff --git a/build.gradle b/build.gradle index e8a9a335e..ec2383178 100644 --- a/build.gradle +++ b/build.gradle @@ -36,10 +36,7 @@ repositories { } dependencies { - implementation 'io.nats:nkeys-java:1.5.2' - implementation 'io.nats:jnats-json:1.5.2' - implementation 'io.nats:jwt-java:1.5.2' - + implementation 'net.i2p.crypto:eddsa:0.3.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' testImplementation 'io.nats:jnats-server-runner:1.2.8' testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.12.3' diff --git a/src/examples/java/io/nats/examples/jetstream/NatsJsMirrorSubUseCases.java b/src/examples/java/io/nats/examples/jetstream/NatsJsMirrorSubUseCases.java index 8d14cb8ac..d80648985 100644 --- a/src/examples/java/io/nats/examples/jetstream/NatsJsMirrorSubUseCases.java +++ b/src/examples/java/io/nats/examples/jetstream/NatsJsMirrorSubUseCases.java @@ -24,7 +24,7 @@ import java.time.Duration; import java.util.List; -import static io.nats.client.support.JsonWriteUtils.printFormatted; +import static io.nats.client.support.JsonUtils.printFormatted; import static io.nats.examples.jetstream.NatsJsUtils.publish; /** diff --git a/src/main/java/io/nats/client/NKey.java b/src/main/java/io/nats/client/NKey.java new file mode 100644 index 000000000..b2c93c254 --- /dev/null +++ b/src/main/java/io/nats/client/NKey.java @@ -0,0 +1,741 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client; + +import net.i2p.crypto.eddsa.EdDSAEngine; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.*; +import java.util.Arrays; + +import static io.nats.client.support.Encoding.base32Decode; +import static io.nats.client.support.Encoding.base32Encode; +import static io.nats.client.support.RandomUtils.PRAND; +import static io.nats.client.support.RandomUtils.SRAND; + +class DecodedSeed { + int prefix; + byte[] bytes; +} + +/** + * <p> + * The NATS ecosystem will be moving to Ed25519 keys for identity, + * authentication and authorization for entities such as Accounts, Users, + * Servers and Clusters. + * </p> + * <p> + * NKeys are based on the Ed25519 standard. This signing algorithm provides for + * the use of public and private keys to sign and verify data. NKeys is designed + * to formulate keys in a much friendlier fashion referencing work done in + * cryptocurrencies, specifically Stellar. Bitcoin and others use a form of + * Base58 (or Base58Check) to encode raw keys. Stellar utilizes a more + * traditional Base32 with a CRC16 and a version or prefix byte. NKeys utilizes + * a similar format with one or two prefix bytes. The base32 encoding of these + * prefixes will yield friendly human readable prefixes, e.g. 'N' = server, 'C' + * = cluster, 'O' = operator, 'A' = account, and 'U' = user to help developers + * and administrators quickly identify key types. + * </p> + * <p> + * Each NKey is generated from 32 bytes. These bytes are called the seed and are + * encoded, in the NKey world, into a string starting with the letter 'S', with + * a second character indicating the key’s type, e.g. "SU" is a seed for a u + * er key pair, "SA" is a seed for an account key pair. The seed can be used t + * create the Ed25519 public/private key pair and should be protected as a p + * ivate key. It is equivalent to the private key for a PGP key pair, or the m + * ster password for your password vault. + * </p> + * <p> + * Ed25519 uses the seed bytes to generate a key pair. The pair contains a + * private key, which can be used to sign data, and a public key which can be + * used to verify a signature. The public key can be distributed, and is not + * considered secret. + * </p> + * <p> + * The NKey libraries encode 32 byte public keys using Base32 and a CRC16 + * checksum plus a prefix based on the key type, e.g. U for a user key. + * </p> + * <p> + * The NKey libraries have support for exporting a 64 byte private key. This + * data is encoded into a string starting with the prefix ‘P’ for private. The + * 64 bytes in a private key consists of the 32 bytes of the seed followed by + * he 32 bytes of the public key. Essentially, the private key is redundant sin + * e you can get it back from the seed alone. The NATS team recommends sto + * ing the 32 byte seed and letting the NKey library regenerate anything els + * it needs for signing. + * </p> + * <p> + * The existence of both a seed and a private key can result in confusion. It is + * reasonable to simply think of Ed25519 as having a public key and a private + * seed, and ignore the longer private key concept. In fact, the NKey libraries + * generally expect you to create an NKey from either a public key, to use for + * verification, or a seed, to use for signing. + * </p> + * <p> + * The NATS system will utilize public NKeys for identification, the NATS system + * will never store or even have access to any private keys or seeds. + * Authentication will utilize a challenge-response mechanism based on a + * collection of random bytes called a nonce. + * </p> + * <p> + * Version note - 2.2.0 provided string arguments for seeds, this is not as safe + * as char arrays, so in 2.3.0 we have included a breaking change to char arrays. + * While this is not the proper version choice, NKeys aren't widely used, if at all yet, + * so we are making the change on a minor jump. + * </p> + */ +public class NKey { + + /** + * NKeys use a prefix byte to indicate their intended owner: 'N' = server, 'C' = + * cluster, 'A' = account, and 'U' = user. 'P' is used for private keys. The + * NKey class formalizes these into the enum NKey.Type. + */ + public enum Type { + /** A user NKey. */ + USER(PREFIX_BYTE_USER), + /** An account NKey. */ + ACCOUNT(PREFIX_BYTE_ACCOUNT), + /** A server NKey. */ + SERVER(PREFIX_BYTE_SERVER), + /** An operator NKey. */ + OPERATOR(PREFIX_BYTE_OPERATOR), + /** A cluster NKey. */ + CLUSTER(PREFIX_BYTE_CLUSTER), + /** A private NKey. */ + PRIVATE(PREFIX_BYTE_PRIVATE); + + private final int prefix; + + Type(int prefix) { + this.prefix = prefix; + } + + public static Type fromPrefix(int prefix) { + if (prefix == PREFIX_BYTE_ACCOUNT) { + return ACCOUNT; + } else if (prefix == PREFIX_BYTE_SERVER) { + return SERVER; + } else if (prefix == PREFIX_BYTE_USER) { + return USER; + } else if (prefix == PREFIX_BYTE_CLUSTER) { + return CLUSTER; + } else if (prefix == PREFIX_BYTE_PRIVATE) { + return ACCOUNT; + } else if (prefix == PREFIX_BYTE_OPERATOR) { + return OPERATOR; + } + + throw new IllegalArgumentException("Unknown prefix"); + } + } + + // PrefixByteSeed is the prefix byte used for encoded NATS Seeds + private static final int PREFIX_BYTE_SEED = 18 << 3; // Base32-encodes to 'S...' + + // PrefixBytePrivate is the prefix byte used for encoded NATS Private keys + static final int PREFIX_BYTE_PRIVATE = 15 << 3; // Base32-encodes to 'P...' + + // PrefixByteServer is the prefix byte used for encoded NATS Servers + static final int PREFIX_BYTE_SERVER = 13 << 3; // Base32-encodes to 'N...' + + // PrefixByteCluster is the prefix byte used for encoded NATS Clusters + static final int PREFIX_BYTE_CLUSTER = 2 << 3; // Base32-encodes to 'C...' + + // PrefixByteAccount is the prefix byte used for encoded NATS Accounts + static final int PREFIX_BYTE_ACCOUNT = 0; // Base32-encodes to 'A...' + + // PrefixByteUser is the prefix byte used for encoded NATS Users + static final int PREFIX_BYTE_USER = 20 << 3; // Base32-encodes to 'U...' + + // PrefixByteOperator is the prefix byte used for encoded NATS Operators + static final int PREFIX_BYTE_OPERATOR = 14 << 3; // Base32-encodes to 'O...' + + private static final int ED25519_PUBLIC_KEYSIZE = 32; + private static final int ED25519_PRIVATE_KEYSIZE = 64; + private static final int ED25519_SEED_SIZE = 32; + private static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519); + + // XModem CRC based on the go version of NKeys + private final static int[] crc16table = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, + 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, + 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, + 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, + 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, + 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, + 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, + 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, + 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, + 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, + 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, + 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, + 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, + 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, + 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, + 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, + 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, + 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 }; + + static int crc16(byte[] bytes) { + int crc = 0; + + for (byte b : bytes) { + crc = ((crc << 8) & 0xffff) ^ crc16table[((crc >> 8) ^ (b & 0xFF)) & 0x00FF]; + } + + return crc; + } + + + private static boolean checkValidPublicPrefixByte(int prefix) { + switch (prefix) { + case PREFIX_BYTE_SERVER: + case PREFIX_BYTE_CLUSTER: + case PREFIX_BYTE_OPERATOR: + case PREFIX_BYTE_ACCOUNT: + case PREFIX_BYTE_USER: + return true; + } + return false; + } + + static char[] removePaddingAndClear(char[] withPad) { + int i; + + for (i=withPad.length-1;i>=0;i--) { + if (withPad[i] != '=') { + break; + } + } + char[] withoutPad = new char[i+1]; + System.arraycopy(withPad, 0, withoutPad, 0, withoutPad.length); + + for (int j=0;j<withPad.length;j++) { + withPad[j] = '\0'; + } + + return withoutPad; + } + + static char[] encode(Type type, byte[] src) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(type.prefix); + bytes.write(src); + + int crc = crc16(bytes.toByteArray()); + byte[] littleEndian = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) crc).array(); + + bytes.write(littleEndian); + + char[] withPad = base32Encode(bytes.toByteArray()); + return removePaddingAndClear(withPad); + } + + static char[] encodeSeed(Type type, byte[] src) throws IOException { + if (src.length != ED25519_PRIVATE_KEYSIZE && src.length != ED25519_SEED_SIZE) { + throw new IllegalArgumentException("Source is not the correct size for an ED25519 seed"); + } + + // In order to make this human printable for both bytes, we need to do a little + // bit manipulation to setup for base32 encoding which takes 5 bits at a time. + int b1 = PREFIX_BYTE_SEED | (type.prefix >> 5); + int b2 = (type.prefix & 31) << 3; // 31 = 00011111 + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(b1); + bytes.write(b2); + bytes.write(src); + + int crc = crc16(bytes.toByteArray()); + byte[] littleEndian = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) crc).array(); + + bytes.write(littleEndian); + + char[] withPad = base32Encode(bytes.toByteArray()); + return removePaddingAndClear(withPad); + } + + static byte[] decode(char[] src) { + byte[] raw = base32Decode(src); + + if (raw == null || raw.length < 4) { + throw new IllegalArgumentException("Invalid encoding for source string"); + } + + byte[] crcBytes = Arrays.copyOfRange(raw, raw.length - 2, raw.length); + byte[] dataBytes = Arrays.copyOfRange(raw, 0, raw.length - 2); + + int crc = ByteBuffer.wrap(crcBytes).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF; + int actual = crc16(dataBytes); + + if (actual != crc) { + throw new IllegalArgumentException("CRC is invalid"); + } + + return dataBytes; + } + + static byte[] decode(Type expectedType, char[] src, boolean safe) { + byte[] raw = decode(src); + byte[] dataBytes = Arrays.copyOfRange(raw, 1, raw.length); + Type type = NKey.Type.fromPrefix(raw[0] & 0xFF); + + if (type != expectedType) { + if (safe) { + return null; + } + throw new IllegalArgumentException("Unexpected type"); + } + + return dataBytes; + } + + static DecodedSeed decodeSeed(char[] seed) { + byte[] raw = decode(seed); + + // Need to do the reverse here to get back to internal representation. + int b1 = raw[0] & 248; // 248 = 11111000 + int b2 = (raw[0] & 7) << 5 | ((raw[1] & 248) >> 3); // 7 = 00000111 + + if (b1 != PREFIX_BYTE_SEED) { + throw new IllegalArgumentException("Invalid encoding"); + } + + if (!checkValidPublicPrefixByte(b2)) { + throw new IllegalArgumentException("Invalid encoded prefix byte"); + } + + byte[] dataBytes = Arrays.copyOfRange(raw, 2, raw.length); + DecodedSeed retVal = new DecodedSeed(); + retVal.prefix = b2; + retVal.bytes = dataBytes; + return retVal; + } + + private static NKey createPair(Type type, SecureRandom random) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + if (random == null) { + random = SRAND; + } + + byte[] seed = new byte[NKey.ed25519.getCurve().getField().getb() / 8]; + random.nextBytes(seed); + + return createPair(type, seed); + } + + private static NKey createPair(Type type, byte[] seed) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seed, NKey.ed25519); + EdDSAPrivateKey privKey = new EdDSAPrivateKey(privKeySpec); + EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKey.getA(), NKey.ed25519); + EdDSAPublicKey pubKey = new EdDSAPublicKey(pubKeySpec); + byte[] pubBytes = pubKey.getAbyte(); + + byte[] bytes = new byte[pubBytes.length + seed.length]; + System.arraycopy(seed, 0, bytes, 0, seed.length); + System.arraycopy(pubBytes, 0, bytes, seed.length, pubBytes.length); + + char[] encoded = encodeSeed(type, bytes); + return new NKey(type, null, encoded); + } + + /** + * Create an Account NKey from the provided random number generator. + * + * If no random is provided, SecureRandom() will be used to create one. + * + * The new NKey contains the private seed, which should be saved in a secure location. + * + * @param random A secure random provider + * @return the new Nkey + * @throws IOException if the seed cannot be encoded to a string + * @throws NoSuchProviderException if the default secure random cannot be created + * @throws NoSuchAlgorithmException if the default secure random cannot be created + */ + public static NKey createAccount(SecureRandom random) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + return createPair(Type.ACCOUNT, random); + } + + /** + * Create an Cluster NKey from the provided random number generator. + * + * If no random is provided, SecureRandom() will be used to create one. + * + * The new NKey contains the private seed, which should be saved in a secure location. + * + * @param random A secure random provider + * @return the new Nkey + * @throws IOException if the seed cannot be encoded to a string + * @throws NoSuchProviderException if the default secure random cannot be created + * @throws NoSuchAlgorithmException if the default secure random cannot be created + */ + public static NKey createCluster(SecureRandom random) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + return createPair(Type.CLUSTER, random); + } + + /** + * Create an Operator NKey from the provided random number generator. + * + * If no random is provided, SecureRandom() will be used to create one. + * + * The new NKey contains the private seed, which should be saved in a secure location. + * + * @param random A secure random provider + * @return the new Nkey + * @throws IOException if the seed cannot be encoded to a string + * @throws NoSuchProviderException if the default secure random cannot be created + * @throws NoSuchAlgorithmException if the default secure random cannot be created + */ + public static NKey createOperator(SecureRandom random) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + return createPair(Type.OPERATOR, random); + } + + /** + * Create a Server NKey from the provided random number generator. + * + * If no random is provided, SecureRandom() will be used to create one. + * + * The new NKey contains the private seed, which should be saved in a secure location. + * + * @param random A secure random provider + * @return the new Nkey + * @throws IOException if the seed cannot be encoded to a string + * @throws NoSuchProviderException if the default secure random cannot be created + * @throws NoSuchAlgorithmException if the default secure random cannot be created + */ + public static NKey createServer(SecureRandom random) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + return createPair(Type.SERVER, random); + } + + /** + * Create a User NKey from the provided random number generator. + * + * If no random is provided, SecureRandom() will be used to create one. + * + * The new NKey contains the private seed, which should be saved in a secure location. + * + * @param random A secure random provider + * @return the new Nkey + * @throws IOException if the seed cannot be encoded to a string + * @throws NoSuchProviderException if the default secure random cannot be created + * @throws NoSuchAlgorithmException if the default secure random cannot be created + */ + public static NKey createUser(SecureRandom random) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException { + return createPair(Type.USER, random); + } + + /** + * Create an NKey object from the encoded public key. This NKey can be used for verification but not for signing. + * + * @param publicKey the string encoded public key + * @return the new Nkey + */ + public static NKey fromPublicKey(char[] publicKey) { + byte[] raw = decode(publicKey); + int prefix = raw[0] & 0xFF; + + if (!checkValidPublicPrefixByte(prefix)) { + throw new IllegalArgumentException("Not a valid public NKey"); + } + + Type type = NKey.Type.fromPrefix(prefix); + return new NKey(type, publicKey, null); + } + + /** + * Creates an NKey object from a string encoded seed. This NKey can be used to sign or verify. + * + * @param seed the string encoded seed, see {@link NKey#getSeed() getSeed()} + * @return the Nkey + */ + public static NKey fromSeed(char[] seed) { + DecodedSeed decoded = decodeSeed(seed); // Should throw on bad seed + + if (decoded.bytes.length == ED25519_PRIVATE_KEYSIZE) { + return new NKey(Type.fromPrefix(decoded.prefix), null, seed); + } else { + try { + return createPair(Type.fromPrefix(decoded.prefix), decoded.bytes); + } catch (Exception e) { + throw new IllegalArgumentException("Bad seed value", e); + } + } + } + + /** + * @param src the encoded public key + * @return true if the public key is an account public key + */ + public static boolean isValidPublicAccountKey(char[] src) { + return decode(Type.ACCOUNT, src, true) != null; + } + + /** + * @param src the encoded public key + * @return true if the public key is a cluster public key + */ + public static boolean isValidPublicClusterKey(char[] src) { + return decode(Type.CLUSTER, src, true) != null; + } + + /** + * @param src the encoded public key + * @return true if the public key is an operator public key + */ + public static boolean isValidPublicOperatorKey(char[] src) { + return decode(Type.OPERATOR, src, true) != null; + } + + /** + * @param src the encoded public key + * @return true if the public key is a server public key + */ + public static boolean isValidPublicServerKey(char[] src) { + return decode(Type.SERVER, src, true) != null; + } + + /** + * @param src the encoded public key + * @return true if the public key is a user public key + */ + public static boolean isValidPublicUserKey(char[] src) { + return decode(Type.USER, src, true) != null; + } + + /** + * The seed or private key per the Ed25519 spec, encoded with encodeSeed. + */ + private char[] privateKeyAsSeed; + + /** + * The public key, maybe null. Used for public only NKeys. + */ + private char[] publicKey; + + private Type type; + + private NKey(Type t, char[] publicKey, char[] privateKey) { + this.type = t; + this.privateKeyAsSeed = privateKey; + this.publicKey = publicKey; + } + + /** + * Clear the seed and public key char arrays by filling them + * with random bytes then zero-ing them out. + * + * The nkey is unusable after this operation. + */ + public void clear() { + if (privateKeyAsSeed != null) { + for (int i=0; i< privateKeyAsSeed.length ; i++) { + privateKeyAsSeed[i] = (char)(PRAND.nextInt(26) + 'a'); + } + Arrays.fill(privateKeyAsSeed, '\0'); + } + if (publicKey != null) { + for (int i=0; i< publicKey.length ; i++) { + publicKey[i] = (char)(PRAND.nextInt(26) + 'a'); + } + Arrays.fill(publicKey, '\0'); + } + } + + /** + * @return the string encoded seed for this NKey + */ + public char[] getSeed() { + if (privateKeyAsSeed == null) { + throw new IllegalStateException("Public-only NKey"); + } + DecodedSeed decoded = decodeSeed(privateKeyAsSeed); + byte[] seedBytes = new byte[ED25519_SEED_SIZE]; + System.arraycopy(decoded.bytes, 0, seedBytes, 0, seedBytes.length); + try { + return encodeSeed(Type.fromPrefix(decoded.prefix), seedBytes); + } catch (Exception e) { + throw new IllegalStateException("Unable to create seed.", e); + } + } + + /** + * @return the encoded public key for this NKey + * + * @throws GeneralSecurityException if there is an encryption problem + * @throws IOException if there is a problem encoding the public + * key + */ + public char[] getPublicKey() throws GeneralSecurityException, IOException { + if (publicKey != null) { + return publicKey; + } + + KeyPair keys = getKeyPair(); + EdDSAPublicKey pubKey = (EdDSAPublicKey) keys.getPublic(); + byte[] pubBytes = pubKey.getAbyte(); + + return encode(this.type, pubBytes); + } + + /** + * @return the encoded private key for this NKey + * + * @throws GeneralSecurityException if there is an encryption problem + * @throws IOException if there is a problem encoding the key + */ + public char[] getPrivateKey() throws GeneralSecurityException, IOException { + if (privateKeyAsSeed == null) { + throw new IllegalStateException("Public-only NKey"); + } + + DecodedSeed decoded = decodeSeed(privateKeyAsSeed); + return encode(Type.PRIVATE, decoded.bytes); + } + + /** + * @return A Java security keypair that represents this NKey in Java security + * form. + * + * @throws GeneralSecurityException if there is an encryption problem + * @throws IOException if there is a problem encoding or decoding + */ + public KeyPair getKeyPair() throws GeneralSecurityException, IOException { + if (privateKeyAsSeed == null) { + throw new IllegalStateException("Public-only NKey"); + } + + DecodedSeed decoded = decodeSeed(privateKeyAsSeed); + byte[] seedBytes = new byte[ED25519_SEED_SIZE]; + byte[] pubBytes = new byte[ED25519_PUBLIC_KEYSIZE]; + + System.arraycopy(decoded.bytes, 0, seedBytes, 0, seedBytes.length); + System.arraycopy(decoded.bytes, seedBytes.length, pubBytes, 0, pubBytes.length); + + EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seedBytes, NKey.ed25519); + EdDSAPrivateKey privKey = new EdDSAPrivateKey(privKeySpec); + EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(pubBytes, NKey.ed25519); + EdDSAPublicKey pubKey = new EdDSAPublicKey(pubKeySpec); + + return new KeyPair(pubKey, privKey); + } + + /** + * @return the Type of this NKey + */ + public Type getType() { + return type; + } + + /** + * Sign aribitrary binary input. + * + * @param input the bytes to sign + * @return the signature for the input from the NKey + * + * @throws GeneralSecurityException if there is an encryption problem + * @throws IOException if there is a problem reading the data + */ + public byte[] sign(byte[] input) throws GeneralSecurityException, IOException { + Signature sgr = new EdDSAEngine(MessageDigest.getInstance(NKey.ed25519.getHashAlgorithm())); + PrivateKey sKey = getKeyPair().getPrivate(); + + sgr.initSign(sKey); + sgr.update(input); + + return sgr.sign(); + } + + /** + * Verify a signature. + * + * @param input the bytes that were signed + * @param signature the bytes for the signature + * @return true if the signature matches this keys signature for the input. + * + * @throws GeneralSecurityException if there is an encryption problem + * @throws IOException if there is a problem reading the data + */ + public boolean verify(byte[] input, byte[] signature) throws GeneralSecurityException, IOException { + Signature sgr = new EdDSAEngine(MessageDigest.getInstance(NKey.ed25519.getHashAlgorithm())); + PublicKey sKey = null; + + if (privateKeyAsSeed != null) { + sKey = getKeyPair().getPublic(); + } else { + char[] encodedPublicKey = getPublicKey(); + byte[] decodedPublicKey = decode(this.type, encodedPublicKey, false); + EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(decodedPublicKey, NKey.ed25519); + sKey = new EdDSAPublicKey(pubKeySpec); + } + + sgr.initVerify(sKey); + sgr.update(input); + + return sgr.verify(signature); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof NKey)) { + return false; + } + + NKey otherNKey = (NKey) o; + + if (this.type != otherNKey.type) { + return false; + } + + if (this.privateKeyAsSeed == null) { + return Arrays.equals(this.publicKey, otherNKey.publicKey); + } + + return Arrays.equals(this.privateKeyAsSeed, otherNKey.privateKeyAsSeed); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + this.type.prefix; + + if (this.privateKeyAsSeed == null) { + result = 31 * result + Arrays.hashCode(this.publicKey); + } else { + result = 31 * result + Arrays.hashCode(this.privateKeyAsSeed); + } + return result; + } + +} \ No newline at end of file diff --git a/src/main/java/io/nats/client/PullRequestOptions.java b/src/main/java/io/nats/client/PullRequestOptions.java index 2c97e7328..e1feb91f0 100644 --- a/src/main/java/io/nats/client/PullRequestOptions.java +++ b/src/main/java/io/nats/client/PullRequestOptions.java @@ -14,11 +14,11 @@ package io.nats.client; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import java.time.Duration; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.Validator.validateGtZero; /** @@ -42,13 +42,13 @@ public PullRequestOptions(Builder b) { @Override public String toJson() { - StringBuilder sb = beginJson(); - addField(sb, BATCH, batchSize); - addField(sb, MAX_BYTES, maxBytes); - addFldWhenTrue(sb, NO_WAIT, noWait); - addFieldAsNanos(sb, EXPIRES, expiresIn); - addFieldAsNanos(sb, IDLE_HEARTBEAT, idleHeartbeat); - return endJson(sb).toString(); + StringBuilder sb = JsonUtils.beginJson(); + JsonUtils.addField(sb, BATCH, batchSize); + JsonUtils.addField(sb, MAX_BYTES, maxBytes); + JsonUtils.addFldWhenTrue(sb, NO_WAIT, noWait); + JsonUtils.addFieldAsNanos(sb, EXPIRES, expiresIn); + JsonUtils.addFieldAsNanos(sb, IDLE_HEARTBEAT, idleHeartbeat); + return JsonUtils.endJson(sb).toString(); } /** diff --git a/src/main/java/io/nats/client/PurgeOptions.java b/src/main/java/io/nats/client/PurgeOptions.java index 9f265d633..ac9138cbc 100644 --- a/src/main/java/io/nats/client/PurgeOptions.java +++ b/src/main/java/io/nats/client/PurgeOptions.java @@ -14,9 +14,11 @@ package io.nats.client; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.Validator.validateSubject; /** @@ -37,9 +39,9 @@ private PurgeOptions(String subject, long seq, long keep) { @Override public String toJson() { StringBuilder sb = beginJson(); - addField(sb, FILTER, subject); - addField(sb, SEQ, seq); - addField(sb, KEEP, keep); + JsonUtils.addField(sb, FILTER, subject); + JsonUtils.addField(sb, SEQ, seq); + JsonUtils.addField(sb, KEEP, keep); return endJson(sb).toString(); } diff --git a/src/main/java/io/nats/client/api/ApiResponse.java b/src/main/java/io/nats/client/api/ApiResponse.java index de6b2f36f..2dbe3a348 100644 --- a/src/main/java/io/nats/client/api/ApiResponse.java +++ b/src/main/java/io/nats/client/api/ApiResponse.java @@ -15,16 +15,12 @@ import io.nats.client.JetStreamApiException; import io.nats.client.Message; -import io.nats.client.support.JsonParseException; -import io.nats.client.support.JsonParser; -import io.nats.client.support.JsonValue; -import io.nats.client.support.JsonValueUtils; +import io.nats.client.support.*; import static io.nats.client.support.ApiConstants.ERROR; import static io.nats.client.support.ApiConstants.TYPE; import static io.nats.client.support.JsonValueUtils.readString; import static io.nats.client.support.JsonValueUtils.readValue; -import static io.nats.client.support.JsonWriteUtils.toKey; public abstract class ApiResponse<T> { @@ -122,6 +118,8 @@ public Error getErrorObject() { @Override public String toString() { - return jv == null ? toKey(getClass()) + "\":null" : jv.toString(getClass()); + return jv == null + ? JsonUtils.toKey(getClass()) + "\":null" + : jv.toString(getClass()); } } diff --git a/src/main/java/io/nats/client/api/ConsumerConfiguration.java b/src/main/java/io/nats/client/api/ConsumerConfiguration.java index e80ceb02d..e3c093e7e 100644 --- a/src/main/java/io/nats/client/api/ConsumerConfiguration.java +++ b/src/main/java/io/nats/client/api/ConsumerConfiguration.java @@ -17,6 +17,7 @@ import io.nats.client.PushSubscribeOptions; import io.nats.client.support.ApiConstants; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import java.time.Duration; @@ -24,8 +25,9 @@ import java.util.*; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.*; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.NatsJetStreamClientError.JsConsumerNameDurableMismatch; import static io.nats.client.support.Validator.*; @@ -202,39 +204,39 @@ protected ConsumerConfiguration(Builder b) */ public String toJson() { StringBuilder sb = beginJson(); - addField(sb, DESCRIPTION, description); - addField(sb, DURABLE_NAME, durable); - addField(sb, NAME, name); - addField(sb, DELIVER_SUBJECT, deliverSubject); - addField(sb, DELIVER_GROUP, deliverGroup); - addField(sb, DELIVER_POLICY, GetOrDefault(deliverPolicy).toString()); - addFieldWhenGtZero(sb, OPT_START_SEQ, startSeq); - addField(sb, OPT_START_TIME, startTime); - addField(sb, ACK_POLICY, GetOrDefault(ackPolicy).toString()); - addFieldAsNanos(sb, ACK_WAIT, ackWait); - addFieldWhenGtZero(sb, MAX_DELIVER, maxDeliver); - addField(sb, MAX_ACK_PENDING, maxAckPending); - addField(sb, REPLAY_POLICY, GetOrDefault(replayPolicy).toString()); - addField(sb, SAMPLE_FREQ, sampleFrequency); - addFieldWhenGtZero(sb, RATE_LIMIT_BPS, rateLimit); - addFieldAsNanos(sb, IDLE_HEARTBEAT, idleHeartbeat); - addFldWhenTrue(sb, FLOW_CONTROL, flowControl); - addField(sb, ApiConstants.MAX_WAITING, maxPullWaiting); - addFldWhenTrue(sb, HEADERS_ONLY, headersOnly); - addField(sb, MAX_BATCH, maxBatch); - addField(sb, MAX_BYTES, maxBytes); - addFieldAsNanos(sb, MAX_EXPIRES, maxExpires); - addFieldAsNanos(sb, INACTIVE_THRESHOLD, inactiveThreshold); - addDurations(sb, BACKOFF, backoff); - addField(sb, NUM_REPLICAS, numReplicas); - addField(sb, MEM_STORAGE, memStorage); - addField(sb, METADATA, metadata); + JsonUtils.addField(sb, DESCRIPTION, description); + JsonUtils.addField(sb, DURABLE_NAME, durable); + JsonUtils.addField(sb, NAME, name); + JsonUtils.addField(sb, DELIVER_SUBJECT, deliverSubject); + JsonUtils.addField(sb, DELIVER_GROUP, deliverGroup); + JsonUtils.addField(sb, DELIVER_POLICY, GetOrDefault(deliverPolicy).toString()); + JsonUtils.addFieldWhenGtZero(sb, OPT_START_SEQ, startSeq); + JsonUtils.addField(sb, OPT_START_TIME, startTime); + JsonUtils.addField(sb, ACK_POLICY, GetOrDefault(ackPolicy).toString()); + JsonUtils.addFieldAsNanos(sb, ACK_WAIT, ackWait); + JsonUtils.addFieldWhenGtZero(sb, MAX_DELIVER, maxDeliver); + JsonUtils.addField(sb, MAX_ACK_PENDING, maxAckPending); + JsonUtils.addField(sb, REPLAY_POLICY, GetOrDefault(replayPolicy).toString()); + JsonUtils.addField(sb, SAMPLE_FREQ, sampleFrequency); + JsonUtils.addFieldWhenGtZero(sb, RATE_LIMIT_BPS, rateLimit); + JsonUtils.addFieldAsNanos(sb, IDLE_HEARTBEAT, idleHeartbeat); + JsonUtils.addFldWhenTrue(sb, FLOW_CONTROL, flowControl); + JsonUtils.addField(sb, ApiConstants.MAX_WAITING, maxPullWaiting); + JsonUtils.addFldWhenTrue(sb, HEADERS_ONLY, headersOnly); + JsonUtils.addField(sb, MAX_BATCH, maxBatch); + JsonUtils.addField(sb, MAX_BYTES, maxBytes); + JsonUtils.addFieldAsNanos(sb, MAX_EXPIRES, maxExpires); + JsonUtils.addFieldAsNanos(sb, INACTIVE_THRESHOLD, inactiveThreshold); + JsonUtils.addDurations(sb, BACKOFF, backoff); + JsonUtils.addField(sb, NUM_REPLICAS, numReplicas); + JsonUtils.addField(sb, MEM_STORAGE, memStorage); + JsonUtils.addField(sb, METADATA, metadata); if (filterSubjects != null) { if (filterSubjects.size() > 1) { - addStrings(sb, FILTER_SUBJECTS, filterSubjects); + JsonUtils.addStrings(sb, FILTER_SUBJECTS, filterSubjects); } else if (filterSubjects.size() == 1) { - addField(sb, FILTER_SUBJECT, filterSubjects.get(0)); + JsonUtils.addField(sb, FILTER_SUBJECT, filterSubjects.get(0)); } } return endJson(sb).toString(); diff --git a/src/main/java/io/nats/client/api/ConsumerCreateRequest.java b/src/main/java/io/nats/client/api/ConsumerCreateRequest.java index 80e339ac7..3b3bb1d88 100644 --- a/src/main/java/io/nats/client/api/ConsumerCreateRequest.java +++ b/src/main/java/io/nats/client/api/ConsumerCreateRequest.java @@ -14,10 +14,11 @@ package io.nats.client.api; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import static io.nats.client.support.ApiConstants.CONFIG; import static io.nats.client.support.ApiConstants.STREAM_NAME; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.*; /** * Object used to make a request to create a consumer. Used Internally @@ -44,7 +45,7 @@ public String toJson() { StringBuilder sb = beginJson(); addField(sb, STREAM_NAME, streamName); - addField(sb, CONFIG, config); + JsonUtils.addField(sb, CONFIG, config); return endJson(sb).toString(); } diff --git a/src/main/java/io/nats/client/api/ConsumerLimits.java b/src/main/java/io/nats/client/api/ConsumerLimits.java index 188191656..c866fbd7c 100644 --- a/src/main/java/io/nats/client/api/ConsumerLimits.java +++ b/src/main/java/io/nats/client/api/ConsumerLimits.java @@ -14,6 +14,7 @@ package io.nats.client.api; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import java.time.Duration; @@ -21,9 +22,10 @@ import static io.nats.client.api.ConsumerConfiguration.*; import static io.nats.client.support.ApiConstants.INACTIVE_THRESHOLD; import static io.nats.client.support.ApiConstants.MAX_ACK_PENDING; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readInteger; import static io.nats.client.support.JsonValueUtils.readNanos; -import static io.nats.client.support.JsonWriteUtils.*; /** * ConsumerLimits @@ -64,8 +66,8 @@ public long getMaxAckPending() { public String toJson() { StringBuilder sb = beginJson(); - addFieldAsNanos(sb, INACTIVE_THRESHOLD, inactiveThreshold); - addField(sb, MAX_ACK_PENDING, maxAckPending); + JsonUtils.addFieldAsNanos(sb, INACTIVE_THRESHOLD, inactiveThreshold); + JsonUtils.addField(sb, MAX_ACK_PENDING, maxAckPending); return endJson(sb).toString(); } diff --git a/src/main/java/io/nats/client/api/External.java b/src/main/java/io/nats/client/api/External.java index 446052010..178150c1a 100644 --- a/src/main/java/io/nats/client/api/External.java +++ b/src/main/java/io/nats/client/api/External.java @@ -19,7 +19,7 @@ import static io.nats.client.support.ApiConstants.API; import static io.nats.client.support.ApiConstants.DELIVER; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.*; /** * External configuration referencing a stream source in another account diff --git a/src/main/java/io/nats/client/api/MessageDeleteRequest.java b/src/main/java/io/nats/client/api/MessageDeleteRequest.java index 39c569968..02ffbe34c 100644 --- a/src/main/java/io/nats/client/api/MessageDeleteRequest.java +++ b/src/main/java/io/nats/client/api/MessageDeleteRequest.java @@ -17,7 +17,7 @@ import static io.nats.client.support.ApiConstants.NO_ERASE; import static io.nats.client.support.ApiConstants.SEQ; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.*; /** * Object used to make a request for message delete requests. diff --git a/src/main/java/io/nats/client/api/MessageGetRequest.java b/src/main/java/io/nats/client/api/MessageGetRequest.java index 7be668cd6..17a2caad3 100644 --- a/src/main/java/io/nats/client/api/MessageGetRequest.java +++ b/src/main/java/io/nats/client/api/MessageGetRequest.java @@ -16,7 +16,7 @@ import io.nats.client.support.JsonSerializable; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.*; /** * Object used to make a request for special message get requests. diff --git a/src/main/java/io/nats/client/api/MessageInfo.java b/src/main/java/io/nats/client/api/MessageInfo.java index 39a4d633a..60374beed 100644 --- a/src/main/java/io/nats/client/api/MessageInfo.java +++ b/src/main/java/io/nats/client/api/MessageInfo.java @@ -17,14 +17,14 @@ import io.nats.client.impl.Headers; import io.nats.client.support.DateTimeUtils; import io.nats.client.support.IncomingHeadersProcessor; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import java.time.ZonedDateTime; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.HeadersUtils.addHeadersAsField; +import static io.nats.client.support.JsonUtils.addRawJson; import static io.nats.client.support.JsonValueUtils.*; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.NatsJetStreamConstants.*; /** @@ -75,7 +75,7 @@ public MessageInfo(Message msg, String streamName, boolean direct) { lastSeq = -1; } else { - lastSeq = safeParseLong(temp, -1); + lastSeq = JsonUtils.safeParseLong(temp, -1); } // these are control headers, not real headers so don't give them to the user. headers = new Headers(msgHeaders, true, MESSAGE_INFO_HEADERS); @@ -160,22 +160,22 @@ public long getLastSeq() { @Override public String toString() { - StringBuilder sb = beginJsonPrefixed("\"MessageInfo\":"); - addField(sb, "direct", direct); - addField(sb, "error", getError()); - addField(sb, SUBJECT, subject); - addField(sb, SEQ, seq); + StringBuilder sb = JsonUtils.beginJsonPrefixed("\"MessageInfo\":"); + JsonUtils.addField(sb, "direct", direct); + JsonUtils.addField(sb, "error", getError()); + JsonUtils.addField(sb, SUBJECT, subject); + JsonUtils.addField(sb, SEQ, seq); if (data == null) { addRawJson(sb, DATA, "null"); } else { - addField(sb, "data_length", data.length); + JsonUtils.addField(sb, "data_length", data.length); } - addField(sb, TIME, time); - addField(sb, STREAM, stream); - addField(sb, "last_seq", lastSeq); - addField(sb, SUBJECT, subject); - addHeadersAsField(sb, HDRS, headers); - return endJson(sb).toString(); + JsonUtils.addField(sb, TIME, time); + JsonUtils.addField(sb, STREAM, stream); + JsonUtils.addField(sb, "last_seq", lastSeq); + JsonUtils.addField(sb, SUBJECT, subject); + JsonUtils.addField(sb, HDRS, headers); + return JsonUtils.endJson(sb).toString(); } } diff --git a/src/main/java/io/nats/client/api/ObjectInfo.java b/src/main/java/io/nats/client/api/ObjectInfo.java index dccb80f6f..7f0123a35 100644 --- a/src/main/java/io/nats/client/api/ObjectInfo.java +++ b/src/main/java/io/nats/client/api/ObjectInfo.java @@ -19,7 +19,8 @@ import java.time.ZonedDateTime; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; /** * The ObjectInfo is Object Meta Information plus instance information @@ -71,12 +72,12 @@ public String toJson() { // never write MTIME (modified) StringBuilder sb = beginJson(); objectMeta.embedJson(sb); // the go code embeds the objectMeta's fields instead of as a child object. - addField(sb, BUCKET, bucket); - addField(sb, NUID, nuid); - addField(sb, SIZE, size); - addField(sb, CHUNKS, chunks); - addField(sb, DIGEST, digest); - addField(sb, DELETED, deleted); + JsonUtils.addField(sb, BUCKET, bucket); + JsonUtils.addField(sb, NUID, nuid); + JsonUtils.addField(sb, SIZE, size); + JsonUtils.addField(sb, CHUNKS, chunks); + JsonUtils.addField(sb, DIGEST, digest); + JsonUtils.addField(sb, DELETED, deleted); return endJson(sb).toString(); } diff --git a/src/main/java/io/nats/client/api/ObjectLink.java b/src/main/java/io/nats/client/api/ObjectLink.java index 19b9c22fa..346db7b7f 100644 --- a/src/main/java/io/nats/client/api/ObjectLink.java +++ b/src/main/java/io/nats/client/api/ObjectLink.java @@ -13,13 +13,15 @@ package io.nats.client.api; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import io.nats.client.support.Validator; import static io.nats.client.support.ApiConstants.BUCKET; import static io.nats.client.support.ApiConstants.NAME; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readString; -import static io.nats.client.support.JsonWriteUtils.*; /** * The ObjectLink is used to embed links to other objects. @@ -46,8 +48,8 @@ private ObjectLink(String bucket, String objectName) { @Override public String toJson() { StringBuilder sb = beginJson(); - addField(sb, BUCKET, bucket); - addField(sb, NAME, objectName); + JsonUtils.addField(sb, BUCKET, bucket); + JsonUtils.addField(sb, NAME, objectName); return endJson(sb).toString(); } diff --git a/src/main/java/io/nats/client/api/ObjectMeta.java b/src/main/java/io/nats/client/api/ObjectMeta.java index ed24bd925..aaf59d273 100644 --- a/src/main/java/io/nats/client/api/ObjectMeta.java +++ b/src/main/java/io/nats/client/api/ObjectMeta.java @@ -14,13 +14,14 @@ import io.nats.client.impl.Headers; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import io.nats.client.support.Validator; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.HeadersUtils.addHeadersAsField; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.*; -import static io.nats.client.support.JsonWriteUtils.*; /** * The ObjectMeta is Object Meta is high level information about an object @@ -59,14 +60,14 @@ public String toJson() { } void embedJson(StringBuilder sb) { - addField(sb, NAME, objectName); - addField(sb, DESCRIPTION, description); - addHeadersAsField(sb, HEADERS, headers); + JsonUtils.addField(sb, NAME, objectName); + JsonUtils.addField(sb, DESCRIPTION, description); + JsonUtils.addField(sb, HEADERS, headers); - // avoid adding an empty child to the json because addField + // avoid adding an empty child to the json because JsonUtils.addField // only checks versus the object being null, which it is never if (objectMetaOptions.hasData()) { - addField(sb, OPTIONS, objectMetaOptions); + JsonUtils.addField(sb, OPTIONS, objectMetaOptions); } } diff --git a/src/main/java/io/nats/client/api/ObjectMetaOptions.java b/src/main/java/io/nats/client/api/ObjectMetaOptions.java index 20f621874..f029a020c 100644 --- a/src/main/java/io/nats/client/api/ObjectMetaOptions.java +++ b/src/main/java/io/nats/client/api/ObjectMetaOptions.java @@ -13,13 +13,15 @@ package io.nats.client.api; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import static io.nats.client.support.ApiConstants.LINK; import static io.nats.client.support.ApiConstants.MAX_CHUNK_SIZE; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readInteger; import static io.nats.client.support.JsonValueUtils.readValue; -import static io.nats.client.support.JsonWriteUtils.*; /** * The ObjectMeta is Object Meta is high level information about an object. @@ -42,8 +44,8 @@ private ObjectMetaOptions(Builder b) { @Override public String toJson() { StringBuilder sb = beginJson(); - addField(sb, LINK, link); - addField(sb, MAX_CHUNK_SIZE, chunkSize); + JsonUtils.addField(sb, LINK, link); + JsonUtils.addField(sb, MAX_CHUNK_SIZE, chunkSize); return endJson(sb).toString(); } diff --git a/src/main/java/io/nats/client/api/Placement.java b/src/main/java/io/nats/client/api/Placement.java index 2473ec971..e90bc6291 100644 --- a/src/main/java/io/nats/client/api/Placement.java +++ b/src/main/java/io/nats/client/api/Placement.java @@ -22,9 +22,9 @@ import static io.nats.client.support.ApiConstants.CLUSTER; import static io.nats.client.support.ApiConstants.TAGS; +import static io.nats.client.support.JsonUtils.*; import static io.nats.client.support.JsonValueUtils.readOptionalStringList; import static io.nats.client.support.JsonValueUtils.readString; -import static io.nats.client.support.JsonWriteUtils.*; /** * Placement directives to consider when placing replicas of a stream diff --git a/src/main/java/io/nats/client/api/Republish.java b/src/main/java/io/nats/client/api/Republish.java index 7e9eb3503..dcf5832c4 100644 --- a/src/main/java/io/nats/client/api/Republish.java +++ b/src/main/java/io/nats/client/api/Republish.java @@ -18,9 +18,9 @@ import io.nats.client.support.Validator; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.*; import static io.nats.client.support.JsonValueUtils.readBoolean; import static io.nats.client.support.JsonValueUtils.readString; -import static io.nats.client.support.JsonWriteUtils.*; /** * Republish Configuration diff --git a/src/main/java/io/nats/client/api/SourceBase.java b/src/main/java/io/nats/client/api/SourceBase.java index c8e786847..6102e5e01 100644 --- a/src/main/java/io/nats/client/api/SourceBase.java +++ b/src/main/java/io/nats/client/api/SourceBase.java @@ -14,6 +14,7 @@ package io.nats.client.api; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import io.nats.client.support.JsonValueUtils; @@ -25,8 +26,9 @@ import static io.nats.client.JetStreamOptions.convertDomainToPrefix; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readValue; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.Validator.consumerFilterSubjectsAreEquivalent; public abstract class SourceBase implements JsonSerializable { @@ -62,12 +64,12 @@ public abstract class SourceBase implements JsonSerializable { */ public String toJson() { StringBuilder sb = beginJson(); - addField(sb, NAME, name); - addFieldWhenGreaterThan(sb, OPT_START_SEQ, startSeq, 0); - addField(sb, OPT_START_TIME, startTime); - addField(sb, FILTER_SUBJECT, filterSubject); - addField(sb, EXTERNAL, external); - addJsons(sb, SUBJECT_TRANSFORMS, subjectTransforms); + JsonUtils.addField(sb, NAME, name); + JsonUtils.addFieldWhenGreaterThan(sb, OPT_START_SEQ, startSeq, 0); + JsonUtils.addField(sb, OPT_START_TIME, startTime); + JsonUtils.addField(sb, FILTER_SUBJECT, filterSubject); + JsonUtils.addField(sb, EXTERNAL, external); + JsonUtils.addJsons(sb, SUBJECT_TRANSFORMS, subjectTransforms); return endJson(sb).toString(); } @@ -109,7 +111,7 @@ public List<SubjectTransform> getSubjectTransforms() { @Override public String toString() { - return toKey(getClass()) + toJson(); + return JsonUtils.toKey(getClass()) + toJson(); } public abstract static class SourceBaseBuilder<T> { diff --git a/src/main/java/io/nats/client/api/StreamConfiguration.java b/src/main/java/io/nats/client/api/StreamConfiguration.java index 5a30f0a67..20ee6b175 100644 --- a/src/main/java/io/nats/client/api/StreamConfiguration.java +++ b/src/main/java/io/nats/client/api/StreamConfiguration.java @@ -13,17 +13,19 @@ package io.nats.client.api; -import io.nats.client.support.JsonParseException; -import io.nats.client.support.JsonParser; -import io.nats.client.support.JsonSerializable; -import io.nats.client.support.JsonValue; +import io.nats.client.support.*; import java.time.Duration; import java.util.*; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.*; +import static io.nats.client.support.JsonValueUtils.readBoolean; +import static io.nats.client.support.JsonValueUtils.readInteger; +import static io.nats.client.support.JsonValueUtils.readLong; +import static io.nats.client.support.JsonValueUtils.readNanos; +import static io.nats.client.support.JsonValueUtils.readString; import static io.nats.client.support.JsonValueUtils.*; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.Validator.*; /** @@ -161,7 +163,7 @@ public String toJson() { StringBuilder sb = beginJson(); addField(sb, NAME, name); - addField(sb, DESCRIPTION, description); + JsonUtils.addField(sb, DESCRIPTION, description); addStrings(sb, SUBJECTS, subjects); addField(sb, RETENTION, retentionPolicy.toString()); addEnumWhenNot(sb, COMPRESSION, compressionOption, CompressionOption.None); diff --git a/src/main/java/io/nats/client/api/StreamInfoOptions.java b/src/main/java/io/nats/client/api/StreamInfoOptions.java index fe383736e..1df84ea88 100644 --- a/src/main/java/io/nats/client/api/StreamInfoOptions.java +++ b/src/main/java/io/nats/client/api/StreamInfoOptions.java @@ -17,7 +17,7 @@ import static io.nats.client.support.ApiConstants.DELETED_DETAILS; import static io.nats.client.support.ApiConstants.SUBJECTS_FILTER; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.*; import static io.nats.client.support.Validator.emptyAsNull; /** diff --git a/src/main/java/io/nats/client/api/SubjectTransform.java b/src/main/java/io/nats/client/api/SubjectTransform.java index a08077dfc..4fb58c3a4 100644 --- a/src/main/java/io/nats/client/api/SubjectTransform.java +++ b/src/main/java/io/nats/client/api/SubjectTransform.java @@ -22,8 +22,8 @@ import static io.nats.client.support.ApiConstants.DEST; import static io.nats.client.support.ApiConstants.SRC; +import static io.nats.client.support.JsonUtils.*; import static io.nats.client.support.JsonValueUtils.readString; -import static io.nats.client.support.JsonWriteUtils.*; /** * SubjectTransform diff --git a/src/main/java/io/nats/client/impl/StreamInfoReader.java b/src/main/java/io/nats/client/impl/StreamInfoReader.java index b2878247e..4b5a3aa61 100644 --- a/src/main/java/io/nats/client/impl/StreamInfoReader.java +++ b/src/main/java/io/nats/client/impl/StreamInfoReader.java @@ -20,7 +20,7 @@ import static io.nats.client.support.ApiConstants.DELETED_DETAILS; import static io.nats.client.support.ApiConstants.SUBJECTS_FILTER; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.*; class StreamInfoReader { diff --git a/src/main/java/io/nats/client/support/DateTimeUtils.java b/src/main/java/io/nats/client/support/DateTimeUtils.java new file mode 100644 index 000000000..c84f53ac6 --- /dev/null +++ b/src/main/java/io/nats/client/support/DateTimeUtils.java @@ -0,0 +1,80 @@ +// Copyright 2020 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Internal json parsing helpers. + */ +public abstract class DateTimeUtils { + private DateTimeUtils() {} /* ensures cannot be constructed */ + + public static final ZoneId ZONE_ID_GMT = ZoneId.of("GMT"); + public static final ZonedDateTime DEFAULT_TIME = ZonedDateTime.of(1, 1, 1, 0, 0, 0, 0, ZONE_ID_GMT); + public static final DateTimeFormatter RFC3339_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnn'Z'"); + + public static ZonedDateTime toGmt(ZonedDateTime zonedDateTime) { + return zonedDateTime.withZoneSameInstant(ZONE_ID_GMT); + } + + public static ZonedDateTime gmtNow() { + return ZonedDateTime.now().withZoneSameInstant(ZONE_ID_GMT); + } + + public static boolean equals(ZonedDateTime zdt1, ZonedDateTime zdt2) { + if (zdt1 == zdt2) return true; + if (zdt1 == null || zdt2 == null) return false; + return zdt1.withZoneSameInstant(ZONE_ID_GMT).equals(zdt2.withZoneSameInstant(ZONE_ID_GMT)); + } + + public static String toRfc3339(ZonedDateTime zonedDateTime) { + return RFC3339_FORMATTER.format(toGmt(zonedDateTime)); + } + + /** + * Parses a date time from the server. + * @param dateTime - date time from the server. + * @return a Zoned Date time. + */ + public static ZonedDateTime parseDateTime(String dateTime) { + return parseDateTime(dateTime, DEFAULT_TIME); + } + + public static ZonedDateTime parseDateTime(String dateTime, ZonedDateTime dflt) { + try { + return toGmt(ZonedDateTime.parse(dateTime)); + } + catch (DateTimeParseException s) { + return dflt; + } + } + + public static ZonedDateTime parseDateTimeThrowParseError(String dateTime) { + return toGmt(ZonedDateTime.parse(dateTime)); + } + + public static ZonedDateTime fromNow(long millis) { + return ZonedDateTime.ofInstant(Instant.now().plusMillis(millis), ZONE_ID_GMT); + } + + public static ZonedDateTime fromNow(Duration dur) { + return ZonedDateTime.ofInstant(Instant.now().plusMillis(dur.toMillis()), ZONE_ID_GMT); + } +} diff --git a/src/main/java/io/nats/client/support/Encoding.java b/src/main/java/io/nats/client/support/Encoding.java new file mode 100644 index 000000000..ab348bab5 --- /dev/null +++ b/src/main/java/io/nats/client/support/Encoding.java @@ -0,0 +1,265 @@ +// Copyright 2020 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +public abstract class Encoding { + private Encoding() {} /* ensures cannot be constructed */ + + /** + * base64 url encode a byte array to a byte array + * @param input the input byte array to encode + * @return the encoded byte array + * @deprecated prefer base64UrlEncode + */ + @Deprecated + public static byte[] base64Encode(byte[] input) { + return Base64.getUrlEncoder().withoutPadding().encode(input); + } + + /** + * base64 url encode a byte array to a byte array + * @param input the input byte array to encode + * @return the encoded byte array + */ + public static byte[] base64UrlEncode(byte[] input) { + return Base64.getUrlEncoder().withoutPadding().encode(input); + } + + /** + * base64 url encode a byte array to a string + * @param input the input byte array to encode + * @return the encoded string + */ + public static String toBase64Url(byte[] input) { + return new String(base64UrlEncode(input)); + } + + /** + * base64 url encode a string to a string + * @param input the input string to encode + * @return the encoded string + */ + public static String toBase64Url(String input) { + return new String(base64UrlEncode(input.getBytes(StandardCharsets.US_ASCII))); + } + + /** + * base64 url decode a byte array + * @param input the input byte array to decode + * @return the decoded byte array + */ + public static byte[] base64UrlDecode(byte[] input) { + return Base64.getUrlDecoder().decode(input); + } + + /** + * get a string from a base64 url encoded byte array + * @param input the input string to decode + * @return the decoded string + */ + public static String fromBase64Url(String input) { + return new String(base64UrlDecode(input.getBytes(StandardCharsets.US_ASCII))); + } + + // http://en.wikipedia.org/wiki/Base_32 + private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final int[] BASE32_LOOKUP; + private static final int MASK = 31; + private static final int SHIFT = 5; + public static char[] base32Encode(final byte[] input) { + int last = input.length; + char[] charBuff = new char[(last + 7) * 8 / SHIFT]; + int offset = 0; + int buffer = input[offset++]; + int bitsLeft = 8; + int i = 0; + + while (bitsLeft > 0 || offset < last) { + if (bitsLeft < SHIFT) { + if (offset < last) { + buffer <<= 8; + buffer |= (input[offset++] & 0xff); + bitsLeft += 8; + } else { + int pad = SHIFT - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + int index = MASK & (buffer >> (bitsLeft - SHIFT)); + bitsLeft -= SHIFT; + charBuff[i] = BASE32_CHARS.charAt(index); + i++; + } + + int nonBlank; + + for (nonBlank=charBuff.length-1;nonBlank>=0;nonBlank--) { + if (charBuff[nonBlank] != 0) { + break; + } + } + + char[] retVal = new char[nonBlank+1]; + + System.arraycopy(charBuff, 0, retVal, 0, retVal.length); + + Arrays.fill(charBuff, '\0'); + + return retVal; + } + static { + BASE32_LOOKUP = new int[256]; + + Arrays.fill(BASE32_LOOKUP, 0xFF); + + for (int i = 0; i < BASE32_CHARS.length(); i++) { + int index = BASE32_CHARS.charAt(i) - '0'; + BASE32_LOOKUP[index] = i; + } + } + + public static byte[] base32Decode(final char[] input) { + byte[] bytes = new byte[input.length * SHIFT / 8]; + int buffer = 0; + int next = 0; + int bitsLeft = 0; + + for (int i = 0; i < input.length; i++) { + int lookup = input[i] - '0'; + + if (lookup < 0 || lookup >= BASE32_LOOKUP.length) { + continue; + } + + int c = BASE32_LOOKUP[lookup]; + buffer <<= SHIFT; + buffer |= c & MASK; + bitsLeft += SHIFT; + if (bitsLeft >= 8) { + bytes[next++] = (byte) (buffer >> (bitsLeft - 8)); + bitsLeft -= 8; + } + } + return bytes; + } + + public static String jsonDecode(String s) { + int len = s.length(); + StringBuilder sb = new StringBuilder(len); + for (int x = 0; x < len; x++) { + char ch = s.charAt(x); + if (ch == '\\') { + char nextChar = (x == len - 1) ? '\\' : s.charAt(x + 1); + switch (nextChar) { + case '\\': + ch = '\\'; + break; + case 'b': + ch = '\b'; + break; + case 'f': + ch = '\f'; + break; + case 'n': + ch = '\n'; + break; + case 'r': + ch = '\r'; + break; + case 't': + ch = '\t'; + break; + // Hex Unicode: u???? + case 'u': + if (x >= len - 5) { + ch = 'u'; + break; + } + int code = Integer.parseInt( + "" + s.charAt(x + 2) + s.charAt(x + 3) + s.charAt(x + 4) + s.charAt(x + 5), 16); + sb.append(Character.toChars(code)); + x += 5; + continue; + default: + ch = nextChar; + break; + } + x++; + } + sb.append(ch); + } + return sb.toString(); + } + + public static String jsonEncode(String s) { + return jsonEncode(new StringBuilder(), s).toString(); + } + + public static StringBuilder jsonEncode(StringBuilder sb, String s) { + int len = s.length(); + for (int x = 0; x < len; x++) { + char ch = s.charAt(x); + switch (ch) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '/': + sb.append("\\/"); + break; + default: + if (ch < ' ') { + sb.append(String.format("\\u%04x", (int) ch)); + } + else { + sb.append(ch); + } + break; + } + } + return sb; + } + + public static String uriDecode(String source) { + try { + return URLDecoder.decode(source.replace("+", "%2B"), "UTF-8"); + } catch (UnsupportedEncodingException e) { + return source; + } + } +} diff --git a/src/main/java/io/nats/client/support/HeadersUtils.java b/src/main/java/io/nats/client/support/HeadersUtils.java deleted file mode 100644 index e6f022980..000000000 --- a/src/main/java/io/nats/client/support/HeadersUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 The NATS Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package io.nats.client.support; - -import io.nats.client.impl.Headers; - -import java.util.List; -import java.util.Map; - -import static io.nats.client.support.JsonWriteUtils.*; - -public abstract class HeadersUtils { - - public static void addHeadersAsField(StringBuilder sb, String fname, Headers headers) { - if (headers != null && !headers.isEmpty()) { - sb.append(Q); - Encoding.jsonEncode(sb, fname); - sb.append("\":{"); - for (Map.Entry<String, List<String>> entry : headers.entrySet()) { - addStrings(sb, entry.getKey(), entry.getValue()); - } - endJson(sb); - sb.append(","); - } - } -} diff --git a/src/main/java/io/nats/client/support/JsonParseException.java b/src/main/java/io/nats/client/support/JsonParseException.java new file mode 100644 index 000000000..2036d46cb --- /dev/null +++ b/src/main/java/io/nats/client/support/JsonParseException.java @@ -0,0 +1,17 @@ +package io.nats.client.support; + +import java.io.IOException; + +public class JsonParseException extends IOException { + public JsonParseException(String message) { + super(message); + } + + public JsonParseException(String message, Throwable cause) { + super(message, cause); + } + + public JsonParseException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/io/nats/client/support/JsonParser.java b/src/main/java/io/nats/client/support/JsonParser.java new file mode 100644 index 000000000..9d4538e61 --- /dev/null +++ b/src/main/java/io/nats/client/support/JsonParser.java @@ -0,0 +1,439 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.nats.client.support.JsonValue.NULL; + +public class JsonParser { + + enum Option {KEEP_NULLS} + + public static JsonValue parse(char[] json) throws JsonParseException { + return new JsonParser(json, 0).parse(); + } + + public static JsonValue parse(char[] json, int startIndex) throws JsonParseException { + return new JsonParser(json, startIndex).parse(); + } + + public static JsonValue parse(char[] json, Option... options) throws JsonParseException { + return new JsonParser(json, 0, options).parse(); + } + + public static JsonValue parse(char[] json, int startIndex, Option... options) throws JsonParseException { + return new JsonParser(json, startIndex, options).parse(); + } + + public static JsonValue parse(String json) throws JsonParseException { + return new JsonParser(json.toCharArray(), 0).parse(); + } + + public static JsonValue parse(String json, int startIndex) throws JsonParseException { + return new JsonParser(json.toCharArray(), startIndex).parse(); + } + + public static JsonValue parse(String json, Option... options) throws JsonParseException { + return new JsonParser(json.toCharArray(), 0, options).parse(); + } + + public static JsonValue parse(String json, int startIndex, Option... options) throws JsonParseException { + return new JsonParser(json.toCharArray(), startIndex, options).parse(); + } + + public static JsonValue parse(byte[] json) throws JsonParseException { + return new JsonParser(new String(json, StandardCharsets.UTF_8).toCharArray(), 0).parse(); + } + + public static JsonValue parse(byte[] json, Option... options) throws JsonParseException { + return new JsonParser(new String(json, StandardCharsets.UTF_8).toCharArray(), 0, options).parse(); + } + + public static JsonValue parseUnchecked(char[] json) { + try { return parse(json); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(char[] json, int startIndex) { + try { return parse(json, startIndex); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(char[] json, Option... options) { + try { return parse(json, options); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(char[] json, int startIndex, Option... options) { + try { return parse(json, startIndex, options); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(String json) { + try { return parse(json); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(String json, int startIndex) { + try { return parse(json, startIndex); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(String json, Option... options) { + try { return parse(json, options); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(String json, int startIndex, Option... options) { + try { return parse(json, startIndex, options); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(byte[] json) { + try { return parse(json); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + public static JsonValue parseUnchecked(byte[] json, Option... options) { + try { return parse(json, options); } + catch (JsonParseException j) { throw new RuntimeException(j); } + } + + private final char[] json; + private final boolean keepNulls; + private final int len; + private int idx; + private int nextIdx; + private char previous; + private char current; + private char next; + + public JsonParser(char[] json) { + this(json, 0); + } + + public JsonParser(char[] json, Option... options) { + this(json, 0, options); + } + + public JsonParser(char[] json, int startIndex, Option... options) { + this.json = json; + + boolean kn = false; + for (Option o : options) { + if (o == Option.KEEP_NULLS) { + kn = true; + break; // b/c only option currently + } + } + keepNulls = kn; + + len = json == null ? 0 : json.length; + idx = startIndex; + if (startIndex < 0) { + throw new IllegalArgumentException("Invalid start index."); + } + nextIdx = -1; + previous = 0; + current = 0; + next = 0; + } + + public JsonValue parse() throws JsonParseException { + char c = peekToken(); + if (c == 0) { + return NULL; + } + return nextValue(); + } + + private JsonValue nextValue() throws JsonParseException { + char c = peekToken(); + if (c == 0) { + throw new JsonParseException("Unexpected end of data."); + } + if (c == '"') { + nextToken(); + return new JsonValue(nextString()); + } + if (c == '{') { + nextToken(); + return new JsonValue(nextObject()); + } + if (c == '[') { + nextToken(); + return new JsonValue(nextArray()); + } + return nextPrimitiveValue(); + } + + private List<JsonValue> nextArray() throws JsonParseException { + List<JsonValue> list = new ArrayList<>(); + char p = peekToken(); + while (p != ']') { + if (p == ',') { + nextToken(); // advance past the peek + } + else { + list.add(nextValue()); + } + p = peekToken(); + } + nextToken(); // advance past the peek + return list; + } + + private JsonValue nextPrimitiveValue() throws JsonParseException { + StringBuilder sb = new StringBuilder(); + char c = peekToken(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(nextToken()); + c = peekToken(); + } + String string = sb.toString(); + if ("true".equalsIgnoreCase(string)) { + return new JsonValue(Boolean.TRUE); + } + if ("false".equalsIgnoreCase(string)) { + return new JsonValue(Boolean.FALSE); + } + if ("null".equalsIgnoreCase(string)) { + return JsonValue.NULL; + } + try { + return asNumber(string); + } + catch (Exception e) { + throw new JsonParseException("Invalid value."); + } + } + + // next object assumes you have already seen the starting { + private Map<String, JsonValue> nextObject() throws JsonParseException { + Map<String, JsonValue> map = new HashMap<>(); + String key; + while (true) { + char c = nextToken(); + switch (c) { + case 0: + throw new JsonParseException("Text must end with '}'"); + case '}': + return map; + case '{': + case '[': + if (previous == '{') { + throw new JsonParseException("Cannot directly nest another Object or Array."); + } + // fall through + default: + key = nextString(); + } + + c = nextToken(); + if (c != ':') { + throw new JsonParseException("Expected a ':' after a key."); + } + + JsonValue value = nextValue(); + if (value != NULL || keepNulls) { + map.put(key, value); + } + + switch (nextToken()) { + case ',': + if (peekToken() == '}') { + return map; // dangling comma + } + break; + case '}': + return map; + default: + throw new JsonParseException("Expected a ',' or '}'."); + } + } + } + + private char nextToken() { + peekToken(); + idx = nextIdx; + nextIdx = -1; + previous = current; + current = next; + next = 0; + return current; + } + + private char nextChar() { + previous = current; + if (idx == len) { + current = 0; + } + else { + current = json[idx++]; + } + next = 0; + nextIdx = -1; + return current; + } + + private char peekToken() { + if (nextIdx == -1) { + nextIdx = idx; + next = 0; + while (nextIdx < len) { + char c = json[nextIdx++]; + switch (c) { + case ' ': + case '\r': + case '\n': + case '\t': + continue; + } + return next = c; + } + } + return next; + } + + // next string assumes you have already seen the starting quote + private String nextString() throws JsonParseException { + StringBuilder sb = new StringBuilder(); + while (true) { + char c = nextChar(); + switch (c) { + case 0: + case '\n': + case '\r': + throw new JsonParseException("Unterminated string."); + case '\\': + c = nextChar(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append(parseU()); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw new JsonParseException("Illegal escape."); + } + break; + default: + if (c == '"') { + return sb.toString(); + } + sb.append(c); + } + } + } + + private char[] parseU() throws JsonParseException { + char[] a = new char[4]; + for (int x = 0; x < 4; x++) { + a[x] = nextToken(); + if (a[x] == 0) { + throw new JsonParseException("Illegal escape."); + } + } + try { + int code = Integer.parseInt("" + a[0] + a[1] + a[2] + a[3], 16); + return Character.toChars(code); + } + catch (RuntimeException e) { + throw new JsonParseException("Illegal escape.", e); + } + } + + private JsonValue asNumber(String val) throws JsonParseException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time to keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return new JsonValue(-0.0); + } + return new JsonValue(bd); + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + double d = Double.parseDouble(val); + if(Double.isNaN(d) || Double.isInfinite(d)) { + throw new JsonParseException("val ["+val+"] is not a valid number."); + } + return new JsonValue(d); + } catch (NumberFormatException ignore) { + throw new JsonParseException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new JsonParseException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new JsonParseException("val ["+val+"] is not a valid number."); + } + } + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return new JsonValue(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return new JsonValue(bi.longValue()); + } + return new JsonValue(bi); + } + throw new JsonParseException("val ["+val+"] is not a valid number."); + } + + private boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } +} diff --git a/src/main/java/io/nats/client/support/JsonSerializable.java b/src/main/java/io/nats/client/support/JsonSerializable.java new file mode 100644 index 000000000..05db533e7 --- /dev/null +++ b/src/main/java/io/nats/client/support/JsonSerializable.java @@ -0,0 +1,28 @@ +// Copyright 2021-2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import java.nio.charset.StandardCharsets; + +public interface JsonSerializable { + String toJson(); + + default byte[] serialize() { + return toJson().getBytes(StandardCharsets.UTF_8); + } + + default JsonValue toJsonValue() { + return JsonParser.parseUnchecked(toJson()); + } +} diff --git a/src/main/java/io/nats/client/support/JsonUtils.java b/src/main/java/io/nats/client/support/JsonUtils.java index 82138392f..d7612895e 100644 --- a/src/main/java/io/nats/client/support/JsonUtils.java +++ b/src/main/java/io/nats/client/support/JsonUtils.java @@ -25,14 +25,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.DateTimeUtils.DEFAULT_TIME; +import static io.nats.client.support.Encoding.jsonDecode; +import static io.nats.client.support.Encoding.jsonEncode; +import static io.nats.client.support.JsonValueUtils.instance; import static io.nats.client.support.NatsConstants.COLON; /** - * Internal json reading and writing helpers. - * @deprecated This class has been extracted to the io.nats.client.support.JsonWriteUtils class in the <a href="https://github.com/nats-io/json.java">json.java</a> library. + * Internal json parsing helpers. + * Read helpers deprecated Prefer using the {@link JsonParser} */ -@Deprecated public abstract class JsonUtils { public static final String EMPTY_JSON = "{}"; @@ -44,6 +46,11 @@ public abstract class JsonUtils { private static final String BEFORE_FIELD_RE = "\""; private static final String AFTER_FIELD_RE = "\"\\s*:\\s*"; + private static final String Q = "\""; + private static final String QCOLONQ = "\":\""; + private static final String QCOLON = "\":"; + private static final String QCOMMA = "\","; + private static final String COMMA = ","; public static final String OPENQ = "{\""; public static final String CLOSE = "}"; @@ -52,39 +59,47 @@ private JsonUtils() {} /* ensures cannot be constructed */ // ---------------------------------------------------------------------------------------------------- // BUILD A STRING OF JSON // ---------------------------------------------------------------------------------------------------- - @Deprecated public static StringBuilder beginJson() { - return JsonWriteUtils.beginJson(); + return new StringBuilder("{"); } - @Deprecated public static StringBuilder beginArray() { - return JsonWriteUtils.beginArray(); + return new StringBuilder("["); } - @Deprecated public static StringBuilder beginJsonPrefixed(String prefix) { - return JsonWriteUtils.beginJsonPrefixed(prefix); + return prefix == null ? beginJson() + : new StringBuilder(prefix).append('{'); } - @Deprecated public static StringBuilder endJson(StringBuilder sb) { - return JsonWriteUtils.endJson(sb); + int lastIndex = sb.length() - 1; + if (sb.charAt(lastIndex) == ',') { + sb.setCharAt(lastIndex, '}'); + return sb; + } + sb.append("}"); + return sb; } - @Deprecated public static StringBuilder endArray(StringBuilder sb) { - return JsonWriteUtils.endArray(sb); + int lastIndex = sb.length() - 1; + if (sb.charAt(lastIndex) == ',') { + sb.setCharAt(lastIndex, ']'); + return sb; + } + sb.append("]"); + return sb; } - @Deprecated public static StringBuilder beginFormattedJson() { - return JsonWriteUtils.beginFormattedJson(); + return new StringBuilder("{\n "); } - @Deprecated public static String endFormattedJson(StringBuilder sb) { - return JsonWriteUtils.endFormattedJson(sb); + sb.setLength(sb.length()-1); + sb.append("\n}"); + return sb.toString().replaceAll(",", ",\n "); } /** @@ -93,9 +108,14 @@ public static String endFormattedJson(StringBuilder sb) { * @param fname fieldname * @param json raw json */ - @Deprecated public static void addRawJson(StringBuilder sb, String fname, String json) { - JsonWriteUtils.addRawJson(sb, fname, json); + if (json != null && json.length() > 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON); + sb.append(json); + sb.append(COMMA); + } } /** @@ -104,9 +124,14 @@ public static void addRawJson(StringBuilder sb, String fname, String json) { * @param fname fieldname * @param value field value */ - @Deprecated public static void addField(StringBuilder sb, String fname, String value) { - JsonWriteUtils.addField(sb, fname, value); + if (value != null && value.length() > 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLONQ); + jsonEncode(sb, value); + sb.append(QCOMMA); + } } /** @@ -115,9 +140,15 @@ public static void addField(StringBuilder sb, String fname, String value) { * @param fname fieldname * @param value field value */ - @Deprecated public static void addFieldEvenEmpty(StringBuilder sb, String fname, String value) { - JsonWriteUtils.addFieldEvenEmpty(sb, fname, value); + if (value == null) { + value = ""; + } + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLONQ); + jsonEncode(sb, value); + sb.append(QCOMMA); } /** @@ -126,9 +157,12 @@ public static void addFieldEvenEmpty(StringBuilder sb, String fname, String valu * @param fname fieldname * @param value field value */ - @Deprecated public static void addField(StringBuilder sb, String fname, Boolean value) { - JsonWriteUtils.addField(sb, fname, value); + if (value != null) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value ? "true" : "false").append(COMMA); + } } /** @@ -137,9 +171,10 @@ public static void addField(StringBuilder sb, String fname, Boolean value) { * @param fname fieldname * @param value field value */ - @Deprecated public static void addFldWhenTrue(StringBuilder sb, String fname, Boolean value) { - JsonWriteUtils.addFldWhenTrue(sb, fname, value); + if (value != null && value) { + addField(sb, fname, true); + } } /** @@ -148,9 +183,12 @@ public static void addFldWhenTrue(StringBuilder sb, String fname, Boolean value) * @param fname fieldname * @param value field value */ - @Deprecated public static void addField(StringBuilder sb, String fname, Integer value) { - JsonWriteUtils.addField(sb, fname, value); + if (value != null && value >= 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value).append(COMMA); + } } /** @@ -159,9 +197,12 @@ public static void addField(StringBuilder sb, String fname, Integer value) { * @param fname fieldname * @param value field value */ - @Deprecated public static void addFieldWhenGtZero(StringBuilder sb, String fname, Integer value) { - JsonWriteUtils.addFieldWhenGtZero(sb, fname, value); + if (value != null && value > 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value).append(COMMA); + } } /** @@ -170,9 +211,12 @@ public static void addFieldWhenGtZero(StringBuilder sb, String fname, Integer va * @param fname fieldname * @param value field value */ - @Deprecated public static void addField(StringBuilder sb, String fname, Long value) { - JsonWriteUtils.addField(sb, fname, value); + if (value != null && value >= 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value).append(COMMA); + } } /** @@ -181,9 +225,12 @@ public static void addField(StringBuilder sb, String fname, Long value) { * @param fname fieldname * @param value field value */ - @Deprecated public static void addFieldWhenGtZero(StringBuilder sb, String fname, Long value) { - JsonWriteUtils.addFieldWhenGtZero(sb, fname, value); + if (value != null && value > 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value).append(COMMA); + } } /** @@ -192,9 +239,12 @@ public static void addFieldWhenGtZero(StringBuilder sb, String fname, Long value * @param fname fieldname * @param value field value */ - @Deprecated public static void addFieldWhenGteMinusOne(StringBuilder sb, String fname, Long value) { - JsonWriteUtils.addFieldWhenGteMinusOne(sb, fname, value); + if (value != null && value >= -1) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value).append(COMMA); + } } /** @@ -204,9 +254,12 @@ public static void addFieldWhenGteMinusOne(StringBuilder sb, String fname, Long * @param value field value * @param gt the number the value must be greater than */ - @Deprecated public static void addFieldWhenGreaterThan(StringBuilder sb, String fname, Long value, long gt) { - JsonWriteUtils.addFieldWhenGreaterThan(sb, fname, value, gt); + if (value != null && value > gt) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value).append(COMMA); + } } /** @@ -215,9 +268,12 @@ public static void addFieldWhenGreaterThan(StringBuilder sb, String fname, Long * @param fname fieldname * @param value duration value */ - @Deprecated public static void addFieldAsNanos(StringBuilder sb, String fname, Duration value) { - JsonWriteUtils.addFieldAsNanos(sb, fname, value); + if (value != null && !value.isZero() && !value.isNegative()) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value.toNanos()).append(COMMA); + } } /** @@ -226,20 +282,25 @@ public static void addFieldAsNanos(StringBuilder sb, String fname, Duration valu * @param fname fieldname * @param value JsonSerializable value */ - @Deprecated public static void addField(StringBuilder sb, String fname, JsonSerializable value) { - JsonWriteUtils.addField(sb, fname, value); + if (value != null) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLON).append(value.toJson()).append(COMMA); + } } - @Deprecated public static void addField(StringBuilder sb, String fname, Map<String, String> map) { - JsonWriteUtils.addField(sb, fname, map); + if (map != null && map.size() > 0) { + addField(sb, fname, instance(map)); + } } @SuppressWarnings("rawtypes") - @Deprecated public static void addEnumWhenNot(StringBuilder sb, String fname, Enum e, Enum dontAddIfThis) { - JsonWriteUtils.addEnumWhenNot(sb, fname, e, dontAddIfThis); + if (e != null && e != dontAddIfThis) { + addField(sb, fname, e.toString()); + } } public interface ListAdder<T> { @@ -254,10 +315,9 @@ public interface ListAdder<T> { * @param list value list * @param adder implementation to add value, including its quotes if required */ - @Deprecated public static <T> void _addList(StringBuilder sb, String fname, List<T> list, ListAdder<T> adder) { sb.append(Q); - Encoding.jsonEncode(sb, fname); + jsonEncode(sb, fname); sb.append("\":["); for (int i = 0; i < list.size(); i++) { if (i > 0) { @@ -274,9 +334,10 @@ public static <T> void _addList(StringBuilder sb, String fname, List<T> list, Li * @param fname fieldname * @param strings field value */ - @Deprecated public static void addStrings(StringBuilder sb, String fname, String[] strings) { - JsonWriteUtils.addStrings(sb, fname, strings); + if (strings != null && strings.length > 0) { + _addStrings(sb, fname, Arrays.asList(strings)); + } } /** @@ -285,9 +346,18 @@ public static void addStrings(StringBuilder sb, String fname, String[] strings) * @param fname fieldname * @param strings field value */ - @Deprecated public static void addStrings(StringBuilder sb, String fname, List<String> strings) { - JsonWriteUtils.addStrings(sb, fname, strings); + if (strings != null && strings.size() > 0) { + _addStrings(sb, fname, strings); + } + } + + private static void _addStrings(StringBuilder sb, String fname, List<String> strings) { + _addList(sb, fname, strings, (sbs, s) -> { + sb.append(Q); + jsonEncode(sb, s); + sb.append(Q); + }); } /** @@ -296,9 +366,10 @@ public static void addStrings(StringBuilder sb, String fname, List<String> strin * @param fname fieldname * @param jsons field value */ - @Deprecated public static void addJsons(StringBuilder sb, String fname, List<? extends JsonSerializable> jsons) { - JsonWriteUtils.addJsons(sb, fname, jsons); + if (jsons != null && !jsons.isEmpty()) { + _addList(sb, fname, jsons, (sbs, s) -> sbs.append(s.toJson())); + } } /** @@ -307,9 +378,10 @@ public static void addJsons(StringBuilder sb, String fname, List<? extends JsonS * @param fname fieldname * @param durations list of durations */ - @Deprecated public static void addDurations(StringBuilder sb, String fname, List<Duration> durations) { - JsonWriteUtils.addDurations(sb, fname, durations); + if (durations != null && durations.size() > 0) { + _addList(sb, fname, durations, (sbs, dur) -> sbs.append(dur.toNanos())); + } } /** @@ -318,14 +390,27 @@ public static void addDurations(StringBuilder sb, String fname, List<Duration> d * @param fname fieldname * @param zonedDateTime field value */ - @Deprecated public static void addField(StringBuilder sb, String fname, ZonedDateTime zonedDateTime) { - JsonWriteUtils.addField(sb, fname, zonedDateTime); + if (zonedDateTime != null && !DEFAULT_TIME.equals(zonedDateTime)) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append(QCOLONQ) + .append(DateTimeUtils.toRfc3339(zonedDateTime)) + .append(QCOMMA); + } } - @Deprecated public static void addField(StringBuilder sb, String fname, Headers headers) { - HeadersUtils.addHeadersAsField(sb, fname, headers); + if (headers != null && headers.size() > 0) { + sb.append(Q); + jsonEncode(sb, fname); + sb.append("\":{"); + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + addStrings(sb, entry.getKey(), entry.getValue()); + } + endJson(sb); + sb.append(","); + } } // ---------------------------------------------------------------------------------------------------- @@ -336,9 +421,8 @@ public static String normalize(String s) { return Character.toString(s.charAt(0)).toUpperCase() + s.substring(1).toLowerCase(); } - @Deprecated public static String toKey(Class<?> c) { - return JsonWriteUtils.toKey(c); + return "\"" + c.getSimpleName() + "\":"; } @Deprecated @@ -359,26 +443,80 @@ private static String indent(int level) { * @param o the object * @return the formatted string */ - @Deprecated public static String getFormatted(Object o) { - return JsonWriteUtils.getFormatted(o); + StringBuilder sb = new StringBuilder(); + int level = 0; + int arrayLevel = 0; + boolean lastWasClose = false; + boolean indentNext = true; + String indent = ""; + String s = o.toString(); + for (int x = 0; x < s.length(); x++) { + char c = s.charAt(x); + if (c == '{') { + if (arrayLevel > 0 && lastWasClose) { + sb.append(indent); + } + sb.append(c).append('\n'); + indent = indent(++level); + indentNext = true; + lastWasClose = false; + } + else if (c == '}') { + indent = indent(--level); + sb.append('\n').append(indent).append(c); + lastWasClose = true; + } + else if (c == ',') { + sb.append(",\n"); + indentNext = true; + } + else { + if (c == '[') { + arrayLevel++; + } + else if (c == ']') { + arrayLevel--; + } + if (indentNext) { + if (c != ' ') { + sb.append(indent).append(c); + indentNext = false; + } + } + else { + sb.append(c); + } + lastWasClose = lastWasClose && Character.isWhitespace(c); + } + } + return sb.toString(); } public static void printFormatted(Object o) { - System.out.println(JsonWriteUtils.getFormatted(o)); + System.out.println(getFormatted(o)); } // ---------------------------------------------------------------------------------------------------- // SAFE NUMBER PARSING HELPERS // ---------------------------------------------------------------------------------------------------- - @Deprecated public static Long safeParseLong(String s) { - return JsonWriteUtils.safeParseLong(s); + try { + return Long.parseLong(s); + } + catch (Exception e1) { + try { + return Long.parseUnsignedLong(s); + } + catch (Exception e2) { + return null; + } + } } - @Deprecated public static long safeParseLong(String s, long dflt) { - return JsonWriteUtils.safeParseLong(s, dflt); + Long l = safeParseLong(s); + return l == null ? dflt : l; } // ---------------------------------------------------------------------------------------------------- @@ -639,8 +777,8 @@ private static List<String> toList(String arrayString) { String[] raw = arrayString.split(","); for (String s : raw) { String cleaned = s.trim().replace("\"", ""); - if (!cleaned.isEmpty()) { - list.add(Encoding.jsonDecode(cleaned)); + if (cleaned.length() > 0) { + list.add(jsonDecode(cleaned)); } } return list; @@ -703,7 +841,7 @@ public static String readString(String json, Pattern pattern) { @Deprecated public static String readString(String json, Pattern pattern, String dflt) { Matcher m = pattern.matcher(json); - return m.find() ? Encoding.jsonDecode(m.group(1)) : dflt; + return m.find() ? jsonDecode(m.group(1)) : dflt; } @Deprecated @@ -732,7 +870,7 @@ else if (c == '"') { sb.append(c); } } - return Encoding.jsonDecode(sb.toString()); + return jsonDecode(sb.toString()); } return dflt; } @@ -834,14 +972,33 @@ public static void readNanos(String json, Pattern pattern, Consumer<Duration> c) } } - @Deprecated public static <T> boolean listEquals(List<T> l1, List<T> l2) { - return Validator.listEquals(l1, l2); + if (l1 == null) + { + return l2 == null; + } + + if (l2 == null) + { + return false; + } + + return l1.equals(l2); } - @Deprecated public static boolean mapEquals(Map<String, String> map1, Map<String, String> map2) { - return Validator.mapEquals(map1, map2); + if (map1 == null) { + return map2 == null; + } + if (map2 == null || map1.size() != map2.size()) { + return false; + } + for (String key : map1.keySet()) { + if (!Objects.equals(map1.get(key), map2.get(key))) { + return false; + } + } + return true; } } diff --git a/src/main/java/io/nats/client/support/JsonValue.java b/src/main/java/io/nats/client/support/JsonValue.java new file mode 100644 index 000000000..ba3e699b4 --- /dev/null +++ b/src/main/java/io/nats/client/support/JsonValue.java @@ -0,0 +1,275 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + +import static io.nats.client.support.JsonUtils.*; + +public class JsonValue implements JsonSerializable { + + public enum Type { + STRING, BOOL, INTEGER, LONG, DOUBLE, FLOAT, BIG_DECIMAL, BIG_INTEGER, MAP, ARRAY, NULL; + } + + private static final char QUOTE = '"'; + private static final char COMMA = ','; + private static final String NULL_STR = "null"; + + public static final JsonValue NULL = new JsonValue(); + public static final JsonValue TRUE = new JsonValue(true); + public static final JsonValue FALSE = new JsonValue(false); + public static final JsonValue EMPTY_MAP = new JsonValue(Collections.unmodifiableMap(new HashMap<>())); + public static final JsonValue EMPTY_ARRAY = new JsonValue(Collections.unmodifiableList(new ArrayList<>())); + + public final String string; + public final Boolean bool; + public final Integer i; + public final Long l; + public final Double d; + public final Float f; + public final BigDecimal bd; + public final BigInteger bi; + public final Map<String, JsonValue> map; + public final List<JsonValue> array; + public final Type type; + public final Object object; + public final Number number; + + public final List<String> mapOrder; + + public JsonValue() { + this(null, null, null, null, null, null, null, null, null, null); + } + + public JsonValue(String string) { + this(string, null, null, null, null, null, null, null, null, null); + } + + public JsonValue(char c) { + this("" + c, null, null, null, null, null, null, null, null, null); + } + + public JsonValue(Boolean bool) { + this(null, bool, null, null, null, null, null, null, null, null); + } + + public JsonValue(int i) { + this(null, null, i, null, null, null, null, null, null, null); + } + + public JsonValue(long l) { + this(null, null, null, l, null, null, null, null, null, null); + } + + public JsonValue(double d) { + this(null, null, null, null, d, null, null, null, null, null); + } + + public JsonValue(float f) { + this(null, null, null, null, null, f, null, null, null, null); + } + + public JsonValue(BigDecimal bd) { + this(null, null, null, null, null, null, bd, null, null, null); + } + + public JsonValue(BigInteger bi) { + this(null, null, null, null, null, null, null, bi, null, null); + } + + public JsonValue(Map<String, JsonValue> map) { + this(null, null, null, null, null, null, null, null, map, null); + } + + public JsonValue(List<JsonValue> list) { + this(null, null, null, null, null, null, null, null, null, list); + } + + public JsonValue(JsonValue[] values) { + this(null, null, null, null, null, null, null, null, null, values == null ? null : Arrays.asList(values)); + } + + private JsonValue(String string, Boolean bool, Integer i, Long l, Double d, Float f, BigDecimal bd, BigInteger bi, Map<String, JsonValue> map, List<JsonValue> array) { + this.map = map; + mapOrder = new ArrayList<>(); + this.array = array; + this.string = string; + this.bool = bool; + this.i = i; + this.l = l; + this.d = d; + this.f = f; + this.bd = bd; + this.bi = bi; + if (i != null) { + this.type = Type.INTEGER; + number = i; + object = number; + } + else if (l != null) { + this.type = Type.LONG; + number = l; + object = number; + } + else if (d != null) { + this.type = Type.DOUBLE; + number = this.d; + object = number; + } + else if (f != null) { + this.type = Type.FLOAT; + number = this.f; + object = number; + } + else if (bd != null) { + this.type = Type.BIG_DECIMAL; + number = this.bd; + object = number; + } + else if (bi != null) { + this.type = Type.BIG_INTEGER; + number = this.bi; + object = number; + } + else { + number = null; + if (map != null) { + this.type = Type.MAP; + object = map; + } + else if (string != null) { + this.type = Type.STRING; + object = string; + } + else if (bool != null) { + this.type = Type.BOOL; + object = bool; + } + else if (array != null) { + this.type = Type.ARRAY; + object = array; + } + else { + this.type = Type.NULL; + object = null; + } + } + } + + public String toString(Class<?> c) { + return toString(c.getSimpleName()); + } + + public String toString(String key) { + return QUOTE + key + QUOTE + ":" + toJson(); + } + + @Override + public String toString() { + return toJson(); + } + + @Override + public JsonValue toJsonValue() { + return this; + } + + @Override + public String toJson() { + switch (type) { + case STRING: return valueString(string); + case BOOL: return valueString(bool); + case MAP: return valueString(map); + case ARRAY: return valueString(array); + case INTEGER: return i.toString(); + case LONG: return l.toString(); + case DOUBLE: return d.toString(); + case FLOAT: return f.toString(); + case BIG_DECIMAL: return bd.toString(); + case BIG_INTEGER: return bi.toString(); + default: return NULL_STR; + } + } + + private String valueString(String s) { + return QUOTE + Encoding.jsonEncode(s) + QUOTE; + } + + private String valueString(boolean b) { + return Boolean.toString(b).toLowerCase(); + } + + private String valueString(Map<String, JsonValue> map) { + StringBuilder sbo = beginJson(); + if (!mapOrder.isEmpty()) { + for (String key : mapOrder) { + addField(sbo, key, map.get(key)); + } + } + else { + for (String key : map.keySet()) { + addField(sbo, key, map.get(key)); + } + } + return endJson(sbo).toString(); + } + + private String valueString(List<JsonValue> list) { + StringBuilder sba = beginArray(); + for (JsonValue v : list) { + sba.append(v.toJson()); + sba.append(COMMA); + } + return endArray(sba).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsonValue jsonValue = (JsonValue) o; + + if (type != jsonValue.type) return false; + if (!Objects.equals(map, jsonValue.map)) return false; + if (!Objects.equals(array, jsonValue.array)) return false; + if (!Objects.equals(string, jsonValue.string)) return false; + if (!Objects.equals(bool, jsonValue.bool)) return false; + if (!Objects.equals(i, jsonValue.i)) return false; + if (!Objects.equals(l, jsonValue.l)) return false; + if (!Objects.equals(d, jsonValue.d)) return false; + if (!Objects.equals(f, jsonValue.f)) return false; + if (!Objects.equals(bd, jsonValue.bd)) return false; + return Objects.equals(bi, jsonValue.bi); + } + + @Override + public int hashCode() { + int result = map != null ? map.hashCode() : 0; + result = 31 * result + (array != null ? array.hashCode() : 0); + result = 31 * result + (string != null ? string.hashCode() : 0); + result = 31 * result + (bool != null ? bool.hashCode() : 0); + result = 31 * result + (i != null ? i.hashCode() : 0); + result = 31 * result + (l != null ? l.hashCode() : 0); + result = 31 * result + (d != null ? d.hashCode() : 0); + result = 31 * result + (f != null ? f.hashCode() : 0); + result = 31 * result + (bd != null ? bd.hashCode() : 0); + result = 31 * result + (bi != null ? bi.hashCode() : 0); + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/io/nats/client/support/JsonValueUtils.java b/src/main/java/io/nats/client/support/JsonValueUtils.java new file mode 100644 index 000000000..b65cbde39 --- /dev/null +++ b/src/main/java/io/nats/client/support/JsonValueUtils.java @@ -0,0 +1,375 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.function.Function; + +import static io.nats.client.support.JsonValue.*; + +/** + * Internal json value helpers. + */ +public abstract class JsonValueUtils { + + private JsonValueUtils() {} /* ensures cannot be constructed */ + + public interface JsonValueSupplier<T> { + T get(JsonValue v); + } + + public static <T> T read(JsonValue jsonValue, String key, JsonValueSupplier<T> valueSupplier) { + JsonValue v = jsonValue == null || jsonValue.map == null ? null : jsonValue.map.get(key); + return valueSupplier.get(v); + } + + public static JsonValue readValue(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> v); + } + + public static JsonValue readObject(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> v == null ? EMPTY_MAP : v); + } + + public static List<JsonValue> readArray(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> v == null ? EMPTY_ARRAY.array : v.array); + } + + public static Map<String, String> readStringStringMap(JsonValue jv, String key) { + JsonValue o = readObject(jv, key); + if (o.type == Type.MAP && o.map.size() > 0) { + Map<String, String> temp = new HashMap<>(); + for (String k : o.map.keySet()) { + String value = readString(o, k); + if (value != null) { + temp.put(k, value); + } + } + return temp.isEmpty() ? null : temp; + } + return null; + } + + public static String readString(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> v == null ? null : v.string); + } + + public static String readString(JsonValue jsonValue, String key, String dflt) { + return read(jsonValue, key, v -> v == null ? dflt : v.string); + } + + public static ZonedDateTime readDate(JsonValue jsonValue, String key) { + return read(jsonValue, key, + v -> v == null || v.string == null ? null : DateTimeUtils.parseDateTimeThrowParseError(v.string)); + } + + public static Integer readInteger(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> v == null ? null : getInteger(v)); + } + + public static int readInteger(JsonValue jsonValue, String key, int dflt) { + return read(jsonValue, key, v -> { + if (v != null) { + Integer i = getInteger(v); + if (i != null) { + return i; + } + } + return dflt; + }); + } + + public static Long readLong(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> v == null ? null : getLong(v)); + } + + public static long readLong(JsonValue jsonValue, String key, long dflt) { + return read(jsonValue, key, v -> { + if (v != null) { + Long l = getLong(v); + if (l != null) { + return l; + } + } + return dflt; + }); + } + + public static boolean readBoolean(JsonValue jsonValue, String key) { + return readBoolean(jsonValue, key, false); + } + + public static Boolean readBoolean(JsonValue jsonValue, String key, Boolean dflt) { + return read(jsonValue, key, + v -> v == null || v.bool == null ? dflt : v.bool); + } + + public static Duration readNanos(JsonValue jsonValue, String key) { + Long l = readLong(jsonValue, key); + return l == null ? null : Duration.ofNanos(l); + } + + public static Duration readNanos(JsonValue jsonValue, String key, Duration dflt) { + Long l = readLong(jsonValue, key); + return l == null ? dflt : Duration.ofNanos(l); + } + + public static <T> List<T> listOf(JsonValue v, Function<JsonValue, T> provider) { + List<T> list = new ArrayList<>(); + if (v != null && v.array != null) { + for (JsonValue jv : v.array) { + T t = provider.apply(jv); + if (t != null) { + list.add(t); + } + } + } + return list; + } + + public static <T> List<T> optionalListOf(JsonValue v, Function<JsonValue, T> provider) { + List<T> list = listOf(v, provider); + return list.isEmpty() ? null : list; + } + + public static List<String> readStringList(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> listOf(v, jv -> jv.string)); + } + + public static List<String> readStringListIgnoreEmpty(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> listOf(v, jv -> { + if (jv.string != null) { + String s = jv.string.trim(); + if (!s.isEmpty()) { + return s; + } + } + return null; + })); + } + + public static List<String> readOptionalStringList(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> optionalListOf(v, jv -> jv.string)); + } + + public static List<Long> readLongList(JsonValue jsonValue, String key) { + return read(jsonValue, key, v -> listOf(v, JsonValueUtils::getLong)); + } + public static List<Duration> readNanosList(JsonValue jsonValue, String key) { + return readNanosList(jsonValue, key, false); + } + + public static List<Duration> readNanosList(JsonValue jsonValue, String key, boolean nullIfEmpty) { + List<Duration> list = read(jsonValue, key, + v -> listOf(v, vv -> { + Long l = getLong(vv); + return l == null ? null : Duration.ofNanos(l); + }) + ); + return list.isEmpty() && nullIfEmpty ? null : list; + } + + public static byte[] readBytes(JsonValue jsonValue, String key) { + String s = readString(jsonValue, key); + return s == null ? null : s.getBytes(StandardCharsets.US_ASCII); + } + + public static byte[] readBase64(JsonValue jsonValue, String key) { + String b64 = readString(jsonValue, key); + return b64 == null ? null : Base64.getDecoder().decode(b64); + } + + public static Integer getInteger(JsonValue v) { + if (v.i != null) { + return v.i; + } + // just in case the number was stored as a long, which is unlikely, but I want to handle it + if (v.l != null && v.l <= Integer.MAX_VALUE && v.l >= Integer.MIN_VALUE) { + return v.l.intValue(); + } + return null; + } + + public static Long getLong(JsonValue v) { + return v.l != null ? v.l : (v.i != null ? (long)v.i : null); + } + + public static long getLong(JsonValue v, long dflt) { + return v.l != null ? v.l : (v.i != null ? (long)v.i : dflt); + } + + public static JsonValue instance(Duration d) { + return new JsonValue(d.toNanos()); + } + + @SuppressWarnings("rawtypes") + public static JsonValue instance(Collection list) { + JsonValue v = new JsonValue(new ArrayList<>()); + for (Object o : list) { + v.array.add(toJsonValue(o)); + } + return v; + } + + @SuppressWarnings("rawtypes") + public static JsonValue instance(Map map) { + JsonValue v = new JsonValue(new HashMap<>()); + for (Object key : map.keySet()) { + v.map.put(key.toString(), toJsonValue(map.get(key))); + } + return v; + } + + public static JsonValue toJsonValue(Object o) { + if (o == null) { + return JsonValue.NULL; + } + if (o instanceof JsonValue) { + return (JsonValue)o; + } + if (o instanceof JsonSerializable) { + return ((JsonSerializable)o).toJsonValue(); + } + if (o instanceof Map) { + //noinspection unchecked,rawtypes + return new JsonValue((Map)o); + } + if (o instanceof List) { + //noinspection unchecked,rawtypes + return new JsonValue((List)o); + } + if (o instanceof Set) { + //noinspection unchecked,rawtypes + return new JsonValue(new ArrayList<>((Set)o)); + } + if (o instanceof String) { + String s = ((String)o).trim(); + return s.length() == 0 ? new JsonValue() : new JsonValue(s); + } + if (o instanceof Boolean) { + return new JsonValue((Boolean)o); + } + if (o instanceof Integer) { + return new JsonValue((Integer)o); + } + if (o instanceof Long) { + return new JsonValue((Long)o); + } + if (o instanceof Double) { + return new JsonValue((Double)o); + } + if (o instanceof Float) { + return new JsonValue((Float)o); + } + if (o instanceof BigDecimal) { + return new JsonValue((BigDecimal)o); + } + if (o instanceof BigInteger) { + return new JsonValue((BigInteger)o); + } + return new JsonValue(o.toString()); + } + + public static MapBuilder mapBuilder() { + return new MapBuilder(); + } + + public static class MapBuilder implements JsonSerializable { + public JsonValue jv; + + public MapBuilder() { + jv = new JsonValue(new HashMap<>()); + } + + public MapBuilder(JsonValue jv) { + this.jv = jv; + } + + public MapBuilder put(String s, Object o) { + if (o != null) { + JsonValue vv = JsonValueUtils.toJsonValue(o); + if (vv.type != JsonValue.Type.NULL) { + jv.map.put(s, vv); + jv.mapOrder.add(s); + } + } + return this; + } + + public MapBuilder put(String s, Map<String, String> stringMap) { + if (stringMap != null) { + MapBuilder mb = new MapBuilder(); + for (String key : stringMap.keySet()) { + mb.put(key, stringMap.get(key)); + } + jv.map.put(s, mb.jv); + jv.mapOrder.add(s); + } + return this; + } + + @Override + public String toJson() { + return jv.toJson(); + } + + @Override + public JsonValue toJsonValue() { + return jv; + } + + @Deprecated + public JsonValue getJsonValue() { + return jv; + } + } + + public static ArrayBuilder arrayBuilder() { + return new ArrayBuilder(); + } + + public static class ArrayBuilder implements JsonSerializable { + public JsonValue jv = new JsonValue(new ArrayList<>()); + public ArrayBuilder add(Object o) { + if (o != null) { + JsonValue vv = JsonValueUtils.toJsonValue(o); + if (vv.type != JsonValue.Type.NULL) { + jv.array.add(JsonValueUtils.toJsonValue(o)); + } + } + return this; + } + + @Override + public String toJson() { + return jv.toJson(); + } + + @Override + public JsonValue toJsonValue() { + return jv; + } + + @Deprecated + public JsonValue getJsonValue() { + return jv; + } + } +} + diff --git a/src/main/java/io/nats/client/support/JwtUtils.java b/src/main/java/io/nats/client/support/JwtUtils.java index 8e318f278..aa324b1e3 100644 --- a/src/main/java/io/nats/client/support/JwtUtils.java +++ b/src/main/java/io/nats/client/support/JwtUtils.java @@ -16,22 +16,28 @@ import io.nats.client.NKey; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.security.MessageDigest; import java.time.Duration; import java.util.List; -import static io.nats.client.support.JsonWriteUtils.beginJson; -import static io.nats.client.support.JsonWriteUtils.endJson; +import static io.nats.client.support.Encoding.*; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; /** * Implements <a href="https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-14.md">ADR-14</a> - * @deprecated This class has been extracted to the io.nats.jwt.JwtUtils class in the <a href="https://github.com/nats-io/jwt.java">jwt.java</a> library. */ -@Deprecated public abstract class JwtUtils { private JwtUtils() {} /* ensures cannot be constructed */ + private static final String ENCODED_CLAIM_HEADER = + toBase64Url("{\"typ\":\"JWT\", \"alg\":\"ed25519-nkey\"}"); + + private static final long NO_LIMIT = -1; + /** * Format string with `%s` placeholder for the JWT token followed * by the user NKey seed. This can be directly used as such: @@ -40,17 +46,27 @@ private JwtUtils() {} /* ensures cannot be constructed */ * NKey userKey = NKey.createUser(new SecureRandom()); * NKey signingKey = loadFromSecretStore(); * String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey())); - * String.format(NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); + * String.format(JwtUtils.NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); * </pre> */ - @Deprecated - public static final String NATS_USER_JWT_FORMAT = io.nats.jwt.JwtUtils.NATS_USER_JWT_FORMAT; + public static final String NATS_USER_JWT_FORMAT = "-----BEGIN NATS USER JWT-----\n" + + "%s\n" + + "------END NATS USER JWT------\n" + + "\n" + + "************************* IMPORTANT *************************\n" + + "NKEY Seed printed below can be used to sign and prove identity.\n" + + "NKEYs are sensitive and should be treated as secrets.\n" + + "\n" + + "-----BEGIN USER NKEY SEED-----\n" + + "%s\n" + + "------END USER NKEY SEED------\n" + + "\n" + + "*************************************************************\n"; /** * Get the current time in seconds since epoch. Used for issue time. * @return the time */ - @Deprecated public static long currentTimeSeconds() { return System.currentTimeMillis() / 1000; } @@ -66,9 +82,8 @@ public static long currentTimeSeconds() { * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueUserJWT(signingKey, publicUserKey, null, null, io.nats.jwt.JwtUtils.currentTimeSeconds(), null, new io.nats.jwt.UserClaim(accountId)); + return issueUserJWT(signingKey, publicUserKey, null, null, currentTimeSeconds(), null, new UserClaim(accountId)); } /** @@ -83,9 +98,8 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueUserJWT(signingKey, publicUserKey, name, null, io.nats.jwt.JwtUtils.currentTimeSeconds(), null, new io.nats.jwt.UserClaim(accountId)); + return issueUserJWT(signingKey, publicUserKey, name, null, currentTimeSeconds(), null, new UserClaim(accountId)); } /** @@ -102,9 +116,8 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String... tags) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueUserJWT(signingKey, accountId, publicUserKey, name, expiration, tags, null, null); + return issueUserJWT(signingKey, publicUserKey, name, expiration, currentTimeSeconds(), null, new UserClaim(accountId).tags(tags)); } /** @@ -122,30 +135,12 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String[] tags, long issuedAt) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueUserJWT(signingKey, accountId, publicUserKey, name, expiration, tags, issuedAt, null); + return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, null, new UserClaim(accountId).tags(tags)); } - /** - * Issue a user JWT from a scoped signing key. See <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a> - * @param signingKey a mandatory account nkey pair to sign the generated jwt. - * @param accountId a mandatory public account nkey. Will throw error when not set or not account nkey. - * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. - * @param name optional human-readable name. When absent, default to publicUserKey. - * @param expiration optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. - * @param tags optional list of tags to be included in the JWT. - * @param issuedAt the current epoch seconds. - * @param audience the optional audience - * @throws IllegalArgumentException if the accountId or publicUserKey is not a valid public key of the proper type - * @throws NullPointerException if signingKey, accountId, or publicUserKey are null. - * @throws GeneralSecurityException if SHA-256 MessageDigest is missing, or if the signingKey can not be used for signing. - * @throws IOException if signingKey sign method throws this exception. - * @return a JWT - */ - @Deprecated public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String[] tags, long issuedAt, String audience) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueUserJWT(signingKey, accountId, publicUserKey, name, expiration, tags, issuedAt, audience); + return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, audience, new UserClaim(accountId).tags(tags)); } /** @@ -162,7 +157,6 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueUserJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, UserClaim nats) throws GeneralSecurityException, IOException { return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, null, nats); } @@ -182,7 +176,6 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueUserJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String audience, UserClaim nats) throws GeneralSecurityException, IOException { // Validate the signingKey: if (signingKey.getType() != NKey.Type.ACCOUNT) { @@ -202,7 +195,7 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String String claimName = Validator.nullOrEmpty(name) ? publicUserKey : name; - return io.nats.jwt.JwtUtils.issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, accSigningKeyPub, audience, nats); + return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, accSigningKeyPub, audience, nats); } /** @@ -218,9 +211,8 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String * @throws IOException if signingKey sign method throws this exception. * @return a JWT */ - @Deprecated public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String accSigningKeyPub, JsonSerializable nats) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueJWT(signingKey, publicUserKey, name, expiration, issuedAt, accSigningKeyPub, null, nats); + return issueJWT(signingKey, publicUserKey, name, expiration, issuedAt, accSigningKeyPub, null, nats); } /** @@ -238,9 +230,35 @@ public static String issueJWT(NKey signingKey, String publicUserKey, String name * @throws GeneralSecurityException if SHA-256 MessageDigest is missing, or if the signingKey can not be used for signing. * @throws IOException if signingKey sign method throws this exception. */ - @Deprecated public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String accSigningKeyPub, String audience, JsonSerializable nats) throws GeneralSecurityException, IOException { - return io.nats.jwt.JwtUtils.issueJWT(signingKey, publicUserKey, name, expiration, issuedAt, accSigningKeyPub, audience, nats); + Claim claim = new Claim(); + claim.aud = audience; + claim.iat = issuedAt; + claim.iss = accSigningKeyPub; + claim.name = name; + claim.sub = publicUserKey; + claim.exp = expiration; + claim.nats = nats; + + // Issue At time is stored in unix seconds + String claimJson = claim.toJson(); + + // Compute jti, a base32 encoded sha256 hash + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + byte[] encoded = sha256.digest(claimJson.getBytes(StandardCharsets.US_ASCII)); + + claim.jti = new String(base32Encode(encoded)); + claimJson = claim.toJson(); + + // all three components (header/body/signature) are base64url encoded + String encBody = toBase64Url(claimJson); + + // compute the signature off of header + body (. included on purpose) + byte[] sig = (ENCODED_CLAIM_HEADER + "." + encBody).getBytes(StandardCharsets.UTF_8); + String encSig = toBase64Url(signingKey.sign(sig)); + + // append signature to header and body and return it + return ENCODED_CLAIM_HEADER + "." + encBody + "." + encSig; } /** @@ -248,12 +266,10 @@ public static String issueJWT(NKey signingKey, String publicUserKey, String name * @param jwt the encoded jwt * @return the claim body json */ - @Deprecated public static String getClaimBody(String jwt) { - return io.nats.jwt.JwtUtils.getClaimBody(jwt); + return fromBase64Url(jwt.split("\\.")[1]); } - @Deprecated public static class UserClaim implements JsonSerializable { public String issuerAccount; // User public String[] tags; // User/GenericFields @@ -265,9 +281,9 @@ public static class UserClaim implements JsonSerializable { public String[] src; // User/UserPermissionLimits/Limits/UserLimits public List<TimeRange> times; // User/UserPermissionLimits/Limits/UserLimits public String locale; // User/UserPermissionLimits/Limits/UserLimits - public long subs = -1; // User/UserPermissionLimits/Limits/NatsLimits - public long data = -1; // User/UserPermissionLimits/Limits/NatsLimits - public long payload = -1; // User/UserPermissionLimits/Limits/NatsLimits + public long subs = NO_LIMIT; // User/UserPermissionLimits/Limits/NatsLimits + public long data = NO_LIMIT; // User/UserPermissionLimits/Limits/NatsLimits + public long payload = NO_LIMIT; // User/UserPermissionLimits/Limits/NatsLimits public boolean bearerToken; // User/UserPermissionLimits public String[] allowedConnectionTypes; // User/UserPermissionLimits @@ -278,21 +294,21 @@ public UserClaim(String issuerAccount) { @Override public String toJson() { StringBuilder sb = beginJson(); - JsonWriteUtils.addField(sb, "issuer_account", issuerAccount); - JsonWriteUtils.addStrings(sb, "tags", tags); - JsonWriteUtils.addField(sb, "type", type); - JsonWriteUtils.addField(sb, "version", version); - JsonWriteUtils.addField(sb, "pub", pub); - JsonWriteUtils.addField(sb, "sub", sub); - JsonWriteUtils.addField(sb, "resp", resp); - JsonWriteUtils.addStrings(sb, "src", src); - JsonWriteUtils.addJsons(sb, "times", times); - JsonWriteUtils.addField(sb, "times_location", locale); - JsonWriteUtils.addFieldWhenGteMinusOne(sb, "subs", subs); - JsonWriteUtils.addFieldWhenGteMinusOne(sb, "data", data); - JsonWriteUtils.addFieldWhenGteMinusOne(sb, "payload", payload); - JsonWriteUtils.addFldWhenTrue(sb, "bearer_token", bearerToken); - JsonWriteUtils.addStrings(sb, "allowed_connection_types", allowedConnectionTypes); + JsonUtils.addField(sb, "issuer_account", issuerAccount); + JsonUtils.addStrings(sb, "tags", tags); + JsonUtils.addField(sb, "type", type); + JsonUtils.addField(sb, "version", version); + JsonUtils.addField(sb, "pub", pub); + JsonUtils.addField(sb, "sub", sub); + JsonUtils.addField(sb, "resp", resp); + JsonUtils.addStrings(sb, "src", src); + JsonUtils.addJsons(sb, "times", times); + JsonUtils.addField(sb, "times_location", locale); + JsonUtils.addFieldWhenGteMinusOne(sb, "subs", subs); + JsonUtils.addFieldWhenGteMinusOne(sb, "data", data); + JsonUtils.addFieldWhenGteMinusOne(sb, "payload", payload); + JsonUtils.addFldWhenTrue(sb, "bearer_token", bearerToken); + JsonUtils.addStrings(sb, "allowed_connection_types", allowedConnectionTypes); return endJson(sb).toString(); } @@ -357,7 +373,6 @@ public UserClaim allowedConnectionTypes(String... allowedConnectionTypes) { } } - @Deprecated public static class TimeRange implements JsonSerializable { public String start; public String end; @@ -370,13 +385,12 @@ public TimeRange(String start, String end) { @Override public String toJson() { StringBuilder sb = beginJson(); - JsonWriteUtils.addField(sb, "start", start); - JsonWriteUtils.addField(sb, "end", end); + JsonUtils.addField(sb, "start", start); + JsonUtils.addField(sb, "end", end); return endJson(sb).toString(); } } - @Deprecated public static class ResponsePermission implements JsonSerializable { public int maxMsgs; public Duration expires; @@ -399,8 +413,8 @@ public ResponsePermission expires(long expiresMillis) { @Override public String toJson() { StringBuilder sb = beginJson(); - JsonWriteUtils.addField(sb, "max", maxMsgs); - JsonWriteUtils.addFieldAsNanos(sb, "ttl", expires); + JsonUtils.addField(sb, "max", maxMsgs); + JsonUtils.addFieldAsNanos(sb, "ttl", expires); return endJson(sb).toString(); } } @@ -422,13 +436,12 @@ public Permission deny(String... deny) { @Override public String toJson() { StringBuilder sb = beginJson(); - JsonWriteUtils.addStrings(sb, "allow", allow); - JsonWriteUtils.addStrings(sb, "deny", deny); + JsonUtils.addStrings(sb, "allow", allow); + JsonUtils.addStrings(sb, "deny", deny); return endJson(sb).toString(); } } - @Deprecated static class Claim implements JsonSerializable { String aud; String jti; @@ -442,19 +455,19 @@ static class Claim implements JsonSerializable { @Override public String toJson() { StringBuilder sb = beginJson(); - JsonWriteUtils.addField(sb, "aud", aud); - JsonWriteUtils.addFieldEvenEmpty(sb, "jti", jti); - JsonWriteUtils.addField(sb, "iat", iat); - JsonWriteUtils.addField(sb, "iss", iss); - JsonWriteUtils.addField(sb, "name", name); - JsonWriteUtils.addField(sb, "sub", sub); + JsonUtils.addField(sb, "aud", aud); + JsonUtils.addFieldEvenEmpty(sb, "jti", jti); + JsonUtils.addField(sb, "iat", iat); + JsonUtils.addField(sb, "iss", iss); + JsonUtils.addField(sb, "name", name); + JsonUtils.addField(sb, "sub", sub); if (exp != null && !exp.isZero() && !exp.isNegative()) { long seconds = exp.toMillis() / 1000; - JsonWriteUtils.addField(sb, "exp", iat + seconds); // relative to the iat + JsonUtils.addField(sb, "exp", iat + seconds); // relative to the iat } - JsonWriteUtils.addField(sb, "nats", nats); + JsonUtils.addField(sb, "nats", nats); return endJson(sb).toString(); } } diff --git a/src/main/java/io/nats/client/support/Validator.java b/src/main/java/io/nats/client/support/Validator.java index c476abdf7..c36659fa7 100644 --- a/src/main/java/io/nats/client/support/Validator.java +++ b/src/main/java/io/nats/client/support/Validator.java @@ -17,7 +17,6 @@ import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -640,33 +639,4 @@ public static boolean mapsAreEquivalent(Map<String, String> m1, Map<String, Stri return true; } - public static <T> boolean listEquals(List<T> l1, List<T> l2) - { - if (l1 == null) - { - return l2 == null; - } - - if (l2 == null) - { - return false; - } - - return l1.equals(l2); - } - - public static boolean mapEquals(Map<String, String> map1, Map<String, String> map2) { - if (map1 == null) { - return map2 == null; - } - if (map2 == null || map1.size() != map2.size()) { - return false; - } - for (String key : map1.keySet()) { - if (!Objects.equals(map1.get(key), map2.get(key))) { - return false; - } - } - return true; - } } diff --git a/src/main/java/io/nats/service/Endpoint.java b/src/main/java/io/nats/service/Endpoint.java index 17ffd79ac..ad566ee29 100644 --- a/src/main/java/io/nats/service/Endpoint.java +++ b/src/main/java/io/nats/service/Endpoint.java @@ -14,6 +14,7 @@ package io.nats.service; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import io.nats.client.support.Validator; @@ -22,9 +23,9 @@ import java.util.Objects; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readString; import static io.nats.client.support.JsonValueUtils.readStringStringMap; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.Validator.validateIsRestrictedTerm; /** @@ -128,17 +129,17 @@ public Endpoint(String name, String subject, String queueGroup, Map<String, Stri @Override public String toJson() { - StringBuilder sb = beginJson(); - addField(sb, NAME, name); - addField(sb, SUBJECT, subject); - addField(sb, QUEUE_GROUP, queueGroup); - addField(sb, METADATA, metadata); + StringBuilder sb = JsonUtils.beginJson(); + JsonUtils.addField(sb, NAME, name); + JsonUtils.addField(sb, SUBJECT, subject); + JsonUtils.addField(sb, QUEUE_GROUP, queueGroup); + JsonUtils.addField(sb, METADATA, metadata); return endJson(sb).toString(); } @Override public String toString() { - return toKey(getClass()) + toJson(); + return JsonUtils.toKey(getClass()) + toJson(); } /** diff --git a/src/main/java/io/nats/service/EndpointStats.java b/src/main/java/io/nats/service/EndpointStats.java index 41f513a92..7e0c095d0 100644 --- a/src/main/java/io/nats/service/EndpointStats.java +++ b/src/main/java/io/nats/service/EndpointStats.java @@ -14,6 +14,7 @@ package io.nats.service; import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import io.nats.client.support.JsonValueUtils; @@ -22,8 +23,9 @@ import java.util.Objects; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.beginJson; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.*; -import static io.nats.client.support.JsonWriteUtils.*; /** * Endpoints stats contains various stats and custom data for an endpoint. @@ -112,16 +114,16 @@ static List<EndpointStats> listOf(JsonValue vEndpointStats) { @Override public String toJson() { StringBuilder sb = beginJson(); - addField(sb, NAME, name); - addField(sb, SUBJECT, subject); - addField(sb, QUEUE_GROUP, queueGroup); - addFieldWhenGtZero(sb, NUM_REQUESTS, numRequests); - addFieldWhenGtZero(sb, NUM_ERRORS, numErrors); - addFieldWhenGtZero(sb, PROCESSING_TIME, processingTime); - addFieldWhenGtZero(sb, AVERAGE_PROCESSING_TIME, averageProcessingTime); - addField(sb, LAST_ERROR, lastError); - addField(sb, DATA, data); - addField(sb, STARTED, started); + JsonUtils.addField(sb, NAME, name); + JsonUtils.addField(sb, SUBJECT, subject); + JsonUtils.addField(sb, QUEUE_GROUP, queueGroup); + JsonUtils.addFieldWhenGtZero(sb, NUM_REQUESTS, numRequests); + JsonUtils.addFieldWhenGtZero(sb, NUM_ERRORS, numErrors); + JsonUtils.addFieldWhenGtZero(sb, PROCESSING_TIME, processingTime); + JsonUtils.addFieldWhenGtZero(sb, AVERAGE_PROCESSING_TIME, averageProcessingTime); + JsonUtils.addField(sb, LAST_ERROR, lastError); + JsonUtils.addField(sb, DATA, data); + JsonUtils.addField(sb, STARTED, started); return endJson(sb).toString(); } @@ -215,7 +217,7 @@ public ZonedDateTime getStarted() { @Override public String toString() { - return toKey(getClass()) + toJson(); + return JsonUtils.toKey(getClass()) + toJson(); } @Override diff --git a/src/main/java/io/nats/service/InfoResponse.java b/src/main/java/io/nats/service/InfoResponse.java index 838e8a022..fbbbb4f80 100644 --- a/src/main/java/io/nats/service/InfoResponse.java +++ b/src/main/java/io/nats/service/InfoResponse.java @@ -13,16 +13,15 @@ package io.nats.service; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; -import io.nats.client.support.Validator; import java.util.*; import static io.nats.client.support.ApiConstants.DESCRIPTION; import static io.nats.client.support.ApiConstants.ENDPOINTS; +import static io.nats.client.support.JsonUtils.listEquals; import static io.nats.client.support.JsonValueUtils.*; -import static io.nats.client.support.JsonWriteUtils.addField; -import static io.nats.client.support.JsonWriteUtils.addJsons; /** * Info response class forms the info json payload, for example: @@ -61,8 +60,8 @@ private InfoResponse(JsonValue jv) { @Override protected void subToJson(StringBuilder sb) { - addField(sb, DESCRIPTION, description); - addJsons(sb, ENDPOINTS, endpoints); + JsonUtils.addField(sb, DESCRIPTION, description); + JsonUtils.addJsons(sb, ENDPOINTS, endpoints); } /** @@ -90,7 +89,7 @@ public boolean equals(Object o) { InfoResponse that = (InfoResponse) o; if (!Objects.equals(description, that.description)) return false; - return Validator.listEquals(endpoints, that.endpoints); + return listEquals(endpoints, that.endpoints); } @Override diff --git a/src/main/java/io/nats/service/Service.java b/src/main/java/io/nats/service/Service.java index bf9dc537a..2f3b34274 100644 --- a/src/main/java/io/nats/service/Service.java +++ b/src/main/java/io/nats/service/Service.java @@ -16,6 +16,7 @@ import io.nats.client.Connection; import io.nats.client.Dispatcher; import io.nats.client.support.DateTimeUtils; +import io.nats.client.support.JsonUtils; import java.time.Duration; import java.time.ZonedDateTime; @@ -27,7 +28,7 @@ import java.util.concurrent.TimeUnit; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.JsonWriteUtils.*; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.Validator.nullOrEmpty; /** @@ -348,11 +349,11 @@ public EndpointStats getEndpointStats(String endpointName) { @Override public String toString() { - StringBuilder sb = beginJsonPrefixed("\"Service\":"); - addField(sb, ID, infoResponse.getId()); - addField(sb, NAME, infoResponse.getName()); - addField(sb, VERSION, infoResponse.getVersion()); - addField(sb, DESCRIPTION, infoResponse.getDescription()); + StringBuilder sb = JsonUtils.beginJsonPrefixed("\"Service\":"); + JsonUtils.addField(sb, ID, infoResponse.getId()); + JsonUtils.addField(sb, NAME, infoResponse.getName()); + JsonUtils.addField(sb, VERSION, infoResponse.getVersion()); + JsonUtils.addField(sb, DESCRIPTION, infoResponse.getDescription()); return endJson(sb).toString(); } } diff --git a/src/main/java/io/nats/service/ServiceResponse.java b/src/main/java/io/nats/service/ServiceResponse.java index be45c9566..9e8bdba56 100644 --- a/src/main/java/io/nats/service/ServiceResponse.java +++ b/src/main/java/io/nats/service/ServiceResponse.java @@ -20,9 +20,9 @@ import java.util.Objects; import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readString; import static io.nats.client.support.JsonValueUtils.readStringStringMap; -import static io.nats.client.support.JsonWriteUtils.*; /** * Base class for service responses Info, Ping and Stats @@ -114,20 +114,20 @@ protected void subToJson(StringBuilder sb) {} @Override public String toJson() { - StringBuilder sb = beginJson(); - addField(sb, ID, id); - addField(sb, NAME, name); - addField(sb, VERSION, version); + StringBuilder sb = JsonUtils.beginJson(); + JsonUtils.addField(sb, ID, id); + JsonUtils.addField(sb, NAME, name); + JsonUtils.addField(sb, VERSION, version); subToJson(sb); - addField(sb, TYPE, type); - addField(sb, METADATA, metadata); + JsonUtils.addField(sb, TYPE, type); + JsonUtils.addField(sb, METADATA, metadata); return endJson(sb).toString(); } @Override public String toString() { - return toKey(getClass()) + toJson(); + return JsonUtils.toKey(getClass()) + toJson(); } @Override @@ -141,7 +141,7 @@ public boolean equals(Object o) { if (!Objects.equals(name, that.name)) return false; if (!Objects.equals(id, that.id)) return false; if (!Objects.equals(version, that.version)) return false; - return Validator.mapEquals(metadata, that.metadata); + return JsonUtils.mapEquals(metadata, that.metadata); } @Override diff --git a/src/main/java/io/nats/service/StatsResponse.java b/src/main/java/io/nats/service/StatsResponse.java index 63855c706..2723d3a4a 100644 --- a/src/main/java/io/nats/service/StatsResponse.java +++ b/src/main/java/io/nats/service/StatsResponse.java @@ -13,6 +13,7 @@ package io.nats.service; +import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; import java.time.ZonedDateTime; @@ -23,8 +24,6 @@ import static io.nats.client.support.ApiConstants.STARTED; import static io.nats.client.support.JsonValueUtils.readDate; import static io.nats.client.support.JsonValueUtils.readValue; -import static io.nats.client.support.JsonWriteUtils.addField; -import static io.nats.client.support.JsonWriteUtils.addJsons; /** * Stats response class forms the stats json payload, for example: @@ -88,8 +87,8 @@ private StatsResponse(JsonValue jv) { @Override protected void subToJson(StringBuilder sb) { - addJsons(sb, ENDPOINTS, endpointStatsList); - addField(sb, STARTED, started); + JsonUtils.addJsons(sb, ENDPOINTS, endpointStatsList); + JsonUtils.addField(sb, STARTED, started); } /** diff --git a/src/test/java/io/nats/client/AuthTests.java b/src/test/java/io/nats/client/AuthTests.java index e6209636d..6be7fb738 100644 --- a/src/test/java/io/nats/client/AuthTests.java +++ b/src/test/java/io/nats/client/AuthTests.java @@ -16,6 +16,7 @@ import io.nats.client.Connection.Status; import io.nats.client.ConnectionListener.Events; import io.nats.client.impl.TestHandler; +import io.nats.client.support.JwtUtils; import io.nats.client.utils.ResourceUtils; import io.nats.client.utils.TestBase; import org.junit.jupiter.api.Test; @@ -31,8 +32,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static io.nats.jwt.JwtUtils.NATS_USER_JWT_FORMAT; -import static io.nats.jwt.JwtUtils.issueUserJWT; import static org.junit.jupiter.api.Assertions.*; public class AuthTests extends TestBase { @@ -714,9 +713,9 @@ else if (error.equalsIgnoreCase("authorization violation")) { long expires = 2500; long wait = 5000; Duration expiration = Duration.ofMillis(expires); - String jwt = issueUserJWT(nKeyAccount, accountId, publicUserKey, "jnatsTestUser", expiration); + String jwt = JwtUtils.issueUserJWT(nKeyAccount, accountId, publicUserKey, "jnatsTestUser", expiration); - String creds = String.format(NATS_USER_JWT_FORMAT, jwt, new String(nKeyUser.getSeed())); + String creds = String.format(JwtUtils.NATS_USER_JWT_FORMAT, jwt, new String(nKeyUser.getSeed())); String credsFile = ResourceUtils.createTempFile("nats_java_test", ".creds", creds.split("\\Q\\n\\E")); try (NatsTestServer ts = new NatsTestServer("src/test/resources/operatorJnatsTest.conf", false)) { diff --git a/src/test/java/io/nats/client/NKeyTests.java b/src/test/java/io/nats/client/NKeyTests.java new file mode 100644 index 000000000..0749b15e9 --- /dev/null +++ b/src/test/java/io/nats/client/NKeyTests.java @@ -0,0 +1,600 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import static io.nats.client.NKey.removePaddingAndClear; +import static io.nats.client.support.Encoding.base32Decode; +import static io.nats.client.support.Encoding.base32Encode; +import static io.nats.client.utils.ResourceUtils.dataAsLines; +import static org.junit.jupiter.api.Assertions.*; + +public class NKeyTests { + private static final int ED25519_SIGNATURE_SIZE = 64; + + @Test + public void testCRC16() { + // Example inputs and outputs from around the web + byte[][] inputs = { + {}, + "abc".getBytes(StandardCharsets.US_ASCII), + "ABC".getBytes(StandardCharsets.US_ASCII), + "This is a string".getBytes(StandardCharsets.US_ASCII), + "123456789".getBytes(StandardCharsets.US_ASCII), + "abcdefghijklmnopqrstuvwxyz0123456789".getBytes(StandardCharsets.US_ASCII), + {(byte) 0x7F}, + {(byte) 0x80}, + {(byte) 0xFF}, + {0x0, 0x1, 0x7D, 0x7E, (byte) 0x7F, (byte) 0x80, (byte) 0xFE, (byte) 0xFF} + }; + + int[] expected = { + 0x0, // "" + 0x9DD6, // "abc" + 0x3994, // "ABC" + 0x21E3, // "This is a string" + 0x31C3, // "123456789" + 0xCBDE, // "abcdefghijklmnopqrstuvwxyz0123456789" + 0x8F78, // 0x7F + 0x9188, // 0x80 + 0x1EF0, // 0xFF + 0xE26F, // {0x0,0x1,0x7D,0x7E, 0x7F, 0x80, 0xFE, 0xFF} + }; + + for (int i = 0; i < inputs.length; i++) { + byte[] input = inputs[i]; + int crc = expected[i]; + int actual = NKey.crc16(input); + assertEquals(crc, actual, String.format("CRC for \"%s\", should be 0x%08X but was 0x%08X", Arrays.toString(input), crc, actual)); + } + } + + @Test + public void testBase32() { + List<String> inputs = dataAsLines("utf8-test-strings.txt"); + + for (String expected : inputs) { + byte[] bytes = expected.getBytes(StandardCharsets.UTF_8); + char[] encoded = base32Encode(bytes); + byte[] decoded = base32Decode(encoded); + String test = new String(decoded, StandardCharsets.UTF_8); + assertEquals(test, expected); + } + + // bad input for coverage + byte[] decoded = base32Decode("/".toCharArray()); + assertEquals(0, decoded.length); + decoded = base32Decode(Character.toChars(512)); + assertEquals(0, decoded.length); + } + + @Test + public void testEncodeDecodeSeed() throws Exception { + byte[] bytes = new byte[64]; + SecureRandom random = new SecureRandom(); + random.nextBytes(bytes); + + char[] encoded = NKey.encodeSeed(NKey.Type.ACCOUNT, bytes); + DecodedSeed decoded = NKey.decodeSeed(encoded); + + assertEquals(NKey.Type.fromPrefix(decoded.prefix), NKey.Type.ACCOUNT); + assertTrue(Arrays.equals(bytes, decoded.bytes)); + } + + @Test + public void testEncodeDecode() throws Exception { + byte[] bytes = new byte[32]; + SecureRandom random = new SecureRandom(); + random.nextBytes(bytes); + + char[] encoded = NKey.encode(NKey.Type.ACCOUNT, bytes); + byte[] decoded = NKey.decode(NKey.Type.ACCOUNT, encoded, false); + assertTrue(Arrays.equals(bytes, decoded)); + + encoded = NKey.encode(NKey.Type.USER, bytes); + decoded = NKey.decode(NKey.Type.USER, encoded, false); + assertTrue(Arrays.equals(bytes, decoded)); + + encoded = NKey.encode(NKey.Type.SERVER, bytes); + decoded = NKey.decode(NKey.Type.SERVER, encoded, false); + assertTrue(Arrays.equals(bytes, decoded)); + + encoded = NKey.encode(NKey.Type.CLUSTER, bytes); + decoded = NKey.decode(NKey.Type.CLUSTER, encoded, false); + assertTrue(Arrays.equals(bytes, decoded)); + } + + @Test + public void testDecodeWrongType() { + assertThrows(IllegalArgumentException.class, () -> { + byte[] bytes = new byte[32]; + SecureRandom random = new SecureRandom(); + random.nextBytes(bytes); + + char[] encoded = NKey.encode(NKey.Type.ACCOUNT, bytes); + NKey.decode(NKey.Type.USER, encoded, false); + }); + } + + @Test + public void testEncodeSeedSize() { + assertThrows(IllegalArgumentException.class, () -> { + byte[] bytes = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(bytes); + + NKey.encodeSeed(NKey.Type.ACCOUNT, bytes); + }); + } + + @Test + public void testDecodeSize() { + assertThrows(IllegalArgumentException.class, () -> NKey.decode(NKey.Type.ACCOUNT, "".toCharArray(), false)); + } + + @Test + public void testBadCRC() throws Exception { + for (int i = 0; i < 10000; i++) { + try { + byte[] bytes = new byte[32]; + SecureRandom random = new SecureRandom(); + random.nextBytes(bytes); + + char[] encoded = NKey.encode(NKey.Type.ACCOUNT, bytes); + + StringBuilder builder = new StringBuilder(); + + for (int j = 0; j < encoded.length; j++) { + if (j == 6) { + char c = encoded[j]; + if (c == 'x' || c == 'X') { + builder.append('Z'); + } else { + builder.append('X'); + } + } else { + builder.append(encoded[j]); + } + } + + NKey.decode(NKey.Type.ACCOUNT, builder.toString().toCharArray(), false); + fail(); + } catch (IllegalArgumentException e) { + //expected + } + } + } + + @Test + public void testAccount() throws Exception { + NKey theKey = NKey.createAccount(null); + assertNotNull(theKey); + + char[] seed = theKey.getSeed(); + NKey.decodeSeed(seed); // throws if there is an issue + + assertEquals(NKey.fromSeed(theKey.getSeed()), NKey.fromSeed(theKey.getSeed())); + + char[] publicKey = theKey.getPublicKey(); + assertEquals(publicKey[0], 'A'); + + char[] privateKey = theKey.getPrivateKey(); + assertEquals(privateKey[0], 'P'); + + byte[] data = "Synadia".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertEquals(sig.length, ED25519_SIGNATURE_SIZE); + + assertTrue(theKey.verify(data, sig)); + + NKey otherKey = NKey.createAccount(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + + assertTrue(NKey.isValidPublicAccountKey(publicKey)); + assertFalse(NKey.isValidPublicClusterKey(publicKey)); + assertFalse(NKey.isValidPublicOperatorKey(publicKey)); + assertFalse(NKey.isValidPublicUserKey(publicKey)); + assertFalse(NKey.isValidPublicServerKey(publicKey)); + } + + @Test + public void testUser() throws Exception { + NKey theKey = NKey.createUser(null); + assertNotNull(theKey); + + char[] seed = theKey.getSeed(); + NKey.decodeSeed(seed); // throws if there is an issue + + assertEquals(NKey.fromSeed(theKey.getSeed()), NKey.fromSeed(theKey.getSeed())); + + char[] publicKey = theKey.getPublicKey(); + assertEquals(publicKey[0], 'U'); + + char[] privateKey = theKey.getPrivateKey(); + assertEquals(privateKey[0], 'P'); + + byte[] data = "Mister Zero".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertEquals(sig.length, ED25519_SIGNATURE_SIZE); + + assertTrue(theKey.verify(data, sig)); + + NKey otherKey = NKey.createUser(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + + assertTrue(NKey.isValidPublicUserKey(publicKey)); + assertFalse(NKey.isValidPublicAccountKey(publicKey)); + assertFalse(NKey.isValidPublicClusterKey(publicKey)); + assertFalse(NKey.isValidPublicOperatorKey(publicKey)); + assertFalse(NKey.isValidPublicServerKey(publicKey)); + } + + @Test + public void testCluster() throws Exception { + NKey theKey = NKey.createCluster(null); + assertNotNull(theKey); + + char[] seed = theKey.getSeed(); + NKey.decodeSeed(seed); // throws if there is an issue + + assertEquals(NKey.fromSeed(theKey.getSeed()), NKey.fromSeed(theKey.getSeed())); + + char[] publicKey = theKey.getPublicKey(); + assertEquals(publicKey[0], 'C'); + + char[] privateKey = theKey.getPrivateKey(); + assertEquals(privateKey[0], 'P'); + + byte[] data = "Connect Everything".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertEquals(sig.length, ED25519_SIGNATURE_SIZE); + + assertTrue(theKey.verify(data, sig)); + + NKey otherKey = NKey.createCluster(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + + assertTrue(NKey.isValidPublicClusterKey(publicKey)); + assertFalse(NKey.isValidPublicAccountKey(publicKey)); + assertFalse(NKey.isValidPublicOperatorKey(publicKey)); + assertFalse(NKey.isValidPublicUserKey(publicKey)); + assertFalse(NKey.isValidPublicServerKey(publicKey)); + } + + @Test + public void testOperator() throws Exception { + NKey theKey = NKey.createOperator(null); + assertNotNull(theKey); + + char[] seed = theKey.getSeed(); + NKey.decodeSeed(seed); // throws if there is an issue + + assertEquals(NKey.fromSeed(theKey.getSeed()), NKey.fromSeed(theKey.getSeed())); + + char[] publicKey = theKey.getPublicKey(); + assertEquals(publicKey[0], 'O'); + + char[] privateKey = theKey.getPrivateKey(); + assertEquals(privateKey[0], 'P'); + + byte[] data = "Connect Everything".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertEquals(sig.length, ED25519_SIGNATURE_SIZE); + + assertTrue(theKey.verify(data, sig)); + + NKey otherKey = NKey.createOperator(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + + assertTrue(NKey.isValidPublicOperatorKey(publicKey)); + assertFalse(NKey.isValidPublicAccountKey(publicKey)); + assertFalse(NKey.isValidPublicClusterKey(publicKey)); + assertFalse(NKey.isValidPublicUserKey(publicKey)); + assertFalse(NKey.isValidPublicServerKey(publicKey)); + } + + @Test + public void testServer() throws Exception { + NKey theKey = NKey.createServer(null); + assertNotNull(theKey); + + char[] seed = theKey.getSeed(); + NKey.decodeSeed(seed); // throws if there is an issue + + assertEquals(NKey.fromSeed(theKey.getSeed()), NKey.fromSeed(theKey.getSeed())); + + char[] publicKey = theKey.getPublicKey(); + assertEquals(publicKey[0], 'N'); + + char[] privateKey = theKey.getPrivateKey(); + assertEquals(privateKey[0], 'P'); + + byte[] data = "Polaris and Pluto".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertEquals(sig.length, ED25519_SIGNATURE_SIZE); + + assertTrue(theKey.verify(data, sig)); + + NKey otherKey = NKey.createServer(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + + assertTrue(NKey.isValidPublicServerKey(publicKey)); + assertFalse(NKey.isValidPublicAccountKey(publicKey)); + assertFalse(NKey.isValidPublicClusterKey(publicKey)); + assertFalse(NKey.isValidPublicOperatorKey(publicKey)); + assertFalse(NKey.isValidPublicUserKey(publicKey)); + } + + @Test + public void testPublicOnly() throws Exception { + NKey theKey = NKey.createUser(null); + assertNotNull(theKey); + + char[] publicKey = theKey.getPublicKey(); + + assertEquals(NKey.fromPublicKey(publicKey), NKey.fromPublicKey(publicKey)); + assertEquals(NKey.fromPublicKey(publicKey).hashCode(), NKey.fromPublicKey(publicKey).hashCode()); + + NKey pubOnly = NKey.fromPublicKey(publicKey); + + assertEquals(pubOnly, pubOnly); // for coverage + + byte[] data = "Public and Private".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertTrue(pubOnly.verify(data, sig)); + + NKey otherKey = NKey.createServer(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + assertNotEquals(otherKey, pubOnly); + + assertNotEquals(pubOnly.getPublicKey()[0], '\0'); + pubOnly.clear(); + assertEquals(pubOnly.getPublicKey()[0], '\0'); + } + + @Test + public void testPublicOnlyCantSign() { + assertThrows(IllegalStateException.class, () -> { + NKey theKey = NKey.createUser(null); + NKey pubOnly = NKey.fromPublicKey(theKey.getPublicKey()); + + byte[] data = "Public and Private".getBytes(StandardCharsets.UTF_8); + pubOnly.sign(data); + }); + } + + @Test + public void testPublicOnlyCantProvideSeed() { + assertThrows(IllegalStateException.class, () -> { + NKey theKey = NKey.createUser(null); + NKey pubOnly = NKey.fromPublicKey(theKey.getPublicKey()); + pubOnly.getSeed(); + }); + } + + @Test + public void testPublicOnlyCantProvidePrivate() { + assertThrows(IllegalStateException.class, () -> { + NKey theKey = NKey.createUser(null); + NKey pubOnly = NKey.fromPublicKey(theKey.getPublicKey()); + pubOnly.getPrivateKey(); + }); + } + + @Test + public void testPublicFromSeedShouldFail() { + assertThrows(IllegalArgumentException.class, () -> { + NKey theKey = NKey.createUser(null); + NKey.fromPublicKey(theKey.getSeed()); + }); + } + + @Test + public void testSeedFromPublicShouldFail() { + assertThrows(IllegalArgumentException.class, () -> { + NKey theKey = NKey.createUser(null); + NKey.fromSeed(theKey.getPublicKey()); + }); + } + + @Test + public void testFromSeed() throws Exception { + NKey theKey = NKey.createAccount(null); + assertNotNull(theKey); + + char[] seed = theKey.getSeed(); + assertEquals(NKey.fromSeed(seed), NKey.fromSeed(seed)); + assertEquals(NKey.fromSeed(seed).hashCode(), NKey.fromSeed(seed).hashCode()); + assertTrue(Arrays.equals(NKey.fromSeed(seed).getPublicKey(), NKey.fromSeed(seed).getPublicKey())); + assertTrue(Arrays.equals(NKey.fromSeed(seed).getPrivateKey(), NKey.fromSeed(seed).getPrivateKey())); + + assertTrue(seed[0] == 'S' && seed[1] == 'A'); + + NKey fromSeed = NKey.fromSeed(seed); + + byte[] data = "Seeds into trees".getBytes(StandardCharsets.UTF_8); + byte[] sig = theKey.sign(data); + + assertTrue(fromSeed.verify(data, sig)); + + NKey otherKey = NKey.createServer(null); + assertFalse(otherKey.verify(data, sig)); + assertNotEquals(otherKey, theKey); + assertNotEquals(otherKey, fromSeed); + } + + @Test + public void testFromBadSeed() { + assertThrows(IllegalArgumentException.class, () -> NKey.fromSeed("BadSeed".toCharArray())); + } + + @Test + public void testFromBadPublicKey() { + assertThrows(IllegalArgumentException.class, () -> NKey.fromPublicKey("BadSeed".toCharArray())); + } + + @Test + public void testBigSignVerify() throws Exception { + NKey theKey = NKey.createAccount(null); + assertNotNull(theKey); + + byte[] data = Files.readAllBytes(Paths.get("src/test/resources/keystore.jks")); + byte[] sig = theKey.sign(data); + + assertEquals(sig.length, ED25519_SIGNATURE_SIZE); + assertTrue(theKey.verify(data, sig)); + + char[] publicKey = theKey.getPublicKey(); + assertTrue(NKey.fromPublicKey(publicKey).verify(data, sig)); + + NKey otherKey = NKey.createUser(null); + byte[] sig2 = otherKey.sign(data); + + assertFalse(otherKey.verify(data, sig)); + assertFalse(Arrays.equals(sig2, sig)); + assertTrue(otherKey.verify(data, sig2)); + } + + /* + Compatibility/Interop data created from the following go code: + user, _ := nkeys.CreateUser(nil) + seed, _ := user.Seed() + publicKey, _ := user.PublicKey() + privateKey, _ := user.PrivateKey() + + data := []byte("Hello World") + sig, _ := user.Sign(data) + encSig := base64.URLEncoding.EncodeToString(sig) + + fmt.Printf("Seed: %q\n", seed) + fmt.Printf("Public: %q\n", publicKey) + fmt.Printf("Private: %q\n", privateKey) + + fmt.Printf("Data: %q\n", data) + fmt.Printf("Signature: %q\n", encSig) + */ + @Test + public void testInterop() throws Exception { + char[] seed = "SUAOXETHU4AZD2424VFDTDJ4TOEUSGZIXMRS6F3MSCMHUUORYHNEVM6ADE".toCharArray(); + char[] publicKey = "UB2YRJYJEFC5GZA5I47TCYYBIXQRAUA6B3MC4SR2WTXNUX6MTYM6BTBP".toCharArray(); + char[] privateKey = "PDVZEZ5HAGI6XGXFJI4Y2PE3RFERWKF3EMXRO3EQTB5FDUOB3JFLG5MIU4ESCROTMQOUOPZRMMAULYIQKAPA5WBOJI5LJ3W2L7GJ4GPAINHQ".toCharArray(); + String encodedSig = "dMSvD2P1Fm6knQGdMwz5h41aPYIOiPqwR-a3b7UNVJr4FcEfFoAIRbm_gtvLGIpplHTc7sZnSMeaS3Ogm1W_CA"; + String nonce = "UkY0TGZNbEVianJZY09F"; + String nonceEncodedSig = "ZNNvu8FDPhpVlyIqjfZGnLCmoAUQggdfdvhGtWLy29AM9TSa6_j15J2iph37j6_FvkGdd1v3crDANwHCqJuQCw"; + byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8); + NKey fromSeed = NKey.fromSeed(seed); + NKey fromPublicKey = NKey.fromPublicKey(publicKey); + + assertEquals(fromSeed.getType(), NKey.Type.USER); + + byte[] nonceData = Base64.getUrlDecoder().decode(nonce); + byte[] nonceSig = Base64.getUrlDecoder().decode(nonceEncodedSig); + byte[] seedNonceSig = fromSeed.sign(nonceData); + String encodedSeedNonceSig = Base64.getUrlEncoder().withoutPadding().encodeToString(seedNonceSig); + + assertTrue(Arrays.equals(seedNonceSig, nonceSig)); + assertEquals(nonceEncodedSig, encodedSeedNonceSig); + + assertTrue(fromSeed.verify(nonceData, nonceSig)); + assertTrue(fromPublicKey.verify(nonceData, nonceSig)); + assertTrue(fromSeed.verify(nonceData, seedNonceSig)); + assertTrue(fromPublicKey.verify(nonceData, seedNonceSig)); + + byte[] seedSig = fromSeed.sign(data); + byte[] sig = Base64.getUrlDecoder().decode(encodedSig); + String encodedSeedSig = Base64.getUrlEncoder().withoutPadding().encodeToString(seedSig); + + assertTrue(Arrays.equals(seedSig, sig)); + assertEquals(encodedSig, encodedSeedSig); + + assertTrue(fromSeed.verify(data, sig)); + assertTrue(fromPublicKey.verify(data, sig)); + assertTrue(fromSeed.verify(data, seedSig)); + assertTrue(fromPublicKey.verify(data, seedSig)); + + // Make sure generation is the same + assertTrue(Arrays.equals(fromSeed.getSeed(), seed)); + assertTrue(Arrays.equals(fromSeed.getPublicKey(), publicKey)); + assertTrue(Arrays.equals(fromSeed.getPrivateKey(), privateKey)); + + DecodedSeed decoded = NKey.decodeSeed(seed); + char[] encodedSeed = NKey.encodeSeed(NKey.Type.fromPrefix(decoded.prefix), decoded.bytes); + assertTrue(Arrays.equals(encodedSeed, seed)); + } + + @Test + public void testTypeEnum() { + assertEquals(NKey.Type.USER, NKey.Type.fromPrefix(NKey.PREFIX_BYTE_USER)); + assertEquals(NKey.Type.ACCOUNT, NKey.Type.fromPrefix(NKey.PREFIX_BYTE_ACCOUNT)); + assertEquals(NKey.Type.SERVER, NKey.Type.fromPrefix(NKey.PREFIX_BYTE_SERVER)); + assertEquals(NKey.Type.OPERATOR, NKey.Type.fromPrefix(NKey.PREFIX_BYTE_OPERATOR)); + assertEquals(NKey.Type.CLUSTER, NKey.Type.fromPrefix(NKey.PREFIX_BYTE_CLUSTER)); + assertEquals(NKey.Type.ACCOUNT, NKey.Type.fromPrefix(NKey.PREFIX_BYTE_PRIVATE)); + assertThrows(IllegalArgumentException.class, () -> { NKey.Type ignored = NKey.Type.fromPrefix(9999); }); + } + + @Test + public void testRemovePaddingAndClear() { + char[] withPad = "!".toCharArray(); + char[] removed = removePaddingAndClear(withPad); + assertEquals(withPad.length, removed.length); + assertEquals('!', removed[0]); + + withPad = "a=".toCharArray(); + removed = removePaddingAndClear(withPad); + assertEquals(1, removed.length); + assertEquals('a', removed[0]); + } + + @Test + public void testEquals() throws Exception { + NKey key = NKey.createServer(null); + assertEquals(key, key); + assertEquals(key, NKey.fromSeed(key.getSeed())); + assertNotEquals(key, new Object()); + assertNotEquals(key, NKey.createServer(null)); + assertNotEquals(key, NKey.createAccount(null)); + } + + @Test + public void testClear() throws Exception { + assertThrows(IllegalArgumentException.class, () -> { + NKey key = NKey.createServer(null); + key.clear(); + key.getPrivateKey(); + + }, "Invalid encoding"); + } +} diff --git a/src/test/java/io/nats/client/api/AccountStatisticsTests.java b/src/test/java/io/nats/client/api/AccountStatisticsTests.java index 5af546d92..a1cb3a4bb 100644 --- a/src/test/java/io/nats/client/api/AccountStatisticsTests.java +++ b/src/test/java/io/nats/client/api/AccountStatisticsTests.java @@ -18,6 +18,7 @@ import java.util.Map; +import static io.nats.client.support.JsonUtils.EMPTY_JSON; import static io.nats.client.utils.ResourceUtils.dataAsString; import static org.junit.jupiter.api.Assertions.*; @@ -58,7 +59,7 @@ public void testAccountStatsImpl() { assertNotNull(as.toString()); // COVERAGE - as = new AccountStatistics(getDataMessage("{}")); + as = new AccountStatistics(getDataMessage(EMPTY_JSON)); assertEquals(0, as.getMemory()); assertEquals(0, as.getStorage()); assertEquals(0, as.getStreams()); diff --git a/src/test/java/io/nats/client/impl/JetStreamPullTests.java b/src/test/java/io/nats/client/impl/JetStreamPullTests.java index 2ee176ee9..5e62e3b45 100644 --- a/src/test/java/io/nats/client/impl/JetStreamPullTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamPullTests.java @@ -16,6 +16,7 @@ import io.nats.client.*; import io.nats.client.api.AckPolicy; import io.nats.client.api.ConsumerConfiguration; +import io.nats.client.support.JsonUtils; import io.nats.client.support.Status; import io.nats.client.utils.TestBase; import org.junit.jupiter.api.Disabled; @@ -31,7 +32,6 @@ import static io.nats.client.api.ConsumerConfiguration.builder; import static io.nats.client.support.ApiConstants.*; -import static io.nats.client.support.JsonWriteUtils.*; import static io.nats.client.support.Status.*; import static org.junit.jupiter.api.Assertions.*; @@ -969,11 +969,11 @@ public BadPullRequestOptions() { @Override public String toJson() { - StringBuilder sb = beginJson(); - addField(sb, BATCH, 1); - addFldWhenTrue(sb, NO_WAIT, true); - addFieldAsNanos(sb, IDLE_HEARTBEAT, Duration.ofMillis(1)); - return endJson(sb).toString(); + StringBuilder sb = JsonUtils.beginJson(); + JsonUtils.addField(sb, BATCH, 1); + JsonUtils.addFldWhenTrue(sb, NO_WAIT, true); + JsonUtils.addFieldAsNanos(sb, IDLE_HEARTBEAT, Duration.ofMillis(1)); + return JsonUtils.endJson(sb).toString(); } } diff --git a/src/test/java/io/nats/client/impl/ListRequestsTests.java b/src/test/java/io/nats/client/impl/ListRequestsTests.java index 8507fdbc7..dc94cd70c 100644 --- a/src/test/java/io/nats/client/impl/ListRequestsTests.java +++ b/src/test/java/io/nats/client/impl/ListRequestsTests.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; +import static io.nats.client.support.JsonUtils.EMPTY_JSON; import static io.nats.client.utils.ResourceUtils.dataAsString; import static org.junit.jupiter.api.Assertions.*; @@ -73,7 +74,7 @@ public void testConsumerListResponse() throws Exception { assertEquals(DateTimeUtils.parseDateTime("2022-06-29T20:33:21.163377Z"), sinfo.getLastActive()); clr = new ConsumerListReader(); - clr.process(getDataMessage("{}")); + clr.process(getDataMessage(EMPTY_JSON)); assertEquals(0, clr.getConsumers().size()); } diff --git a/src/test/java/io/nats/client/support/DateTimeUtilsTests.java b/src/test/java/io/nats/client/support/DateTimeUtilsTests.java new file mode 100644 index 000000000..27e2520ac --- /dev/null +++ b/src/test/java/io/nats/client/support/DateTimeUtilsTests.java @@ -0,0 +1,86 @@ +// Copyright 2015-2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public final class DateTimeUtilsTests { + + @Test + public void testParseDateTime() { + assertEquals(1611186068, DateTimeUtils.parseDateTime("2021-01-20T23:41:08.579594Z").toEpochSecond()); + assertEquals(1612293508, DateTimeUtils.parseDateTime("2021-02-02T11:18:28.347722551-08:00").toEpochSecond()); + assertEquals(DateTimeUtils.DEFAULT_TIME, DateTimeUtils.parseDateTime("anything-not-valid")); + + ZonedDateTime zdt1 = DateTimeUtils.parseDateTime("2021-01-20T18:41:08-05:00"); + ZonedDateTime zdt2 = DateTimeUtils.parseDateTime("2021-01-20T23:41:08.000000Z"); + assertEquals(zdt1, zdt2); + + zdt1 = ZonedDateTime.of(2012, 1, 12, 6, 30, 1, 500, DateTimeUtils.ZONE_ID_GMT); + assertEquals(zdt1.toEpochSecond(), DateTimeUtils.parseDateTime("2012-01-12T06:30:01.000000500Z").toEpochSecond()); + } + + @Test + public void testToRfc3339() { + Instant i = Instant.ofEpochSecond(1611186068); + ZonedDateTime zdt1 = ZonedDateTime.ofInstant(i, ZoneId.systemDefault()); + ZonedDateTime zdt2 = ZonedDateTime.ofInstant(i, DateTimeUtils.ZONE_ID_GMT); + System.out.println(zdt1); + System.out.println(zdt2); + assertEquals(zdt1.toEpochSecond(), zdt2.toEpochSecond()); + + String rfc1 = DateTimeUtils.toRfc3339(zdt1); + String rfc2 = DateTimeUtils.toRfc3339(zdt2); + assertEquals(rfc1, rfc2); + System.out.println(zdt2.toEpochSecond()); + + assertEquals("2021-01-20T23:41:08.579594000Z", DateTimeUtils.toRfc3339(DateTimeUtils.parseDateTime("2021-01-20T23:41:08.579594Z"))); + assertEquals("2021-02-02T19:18:28.347722551Z", DateTimeUtils.toRfc3339(DateTimeUtils.parseDateTime("2021-02-02T11:18:28.347722551-08:00"))); + } + + @Test + public void testFromNow() { + long now = Instant.now().toEpochMilli(); + long then = Instant.from(DateTimeUtils.fromNow(5000)).toEpochMilli(); + assertTrue(then - now < 5050); // it takes about 10 ms to execute fromNow + + now = Instant.now().toEpochMilli(); + then = Instant.from(DateTimeUtils.fromNow(Duration.ofMillis(5000))).toEpochMilli(); + assertTrue(then - now < 5050); + } + + @Test + public void testEquals() { + Instant i = Instant.ofEpochSecond(System.currentTimeMillis()); + ZonedDateTime zdt1 = ZonedDateTime.ofInstant(i, ZoneId.of("America/New_York")); + ZonedDateTime zdt2 = ZonedDateTime.ofInstant(i, DateTimeUtils.ZONE_ID_GMT); + assertTrue(DateTimeUtils.equals(zdt1, zdt1)); + assertTrue(DateTimeUtils.equals(zdt1, zdt2)); + assertFalse(DateTimeUtils.equals(zdt1, null)); + assertFalse(DateTimeUtils.equals(null, zdt2)); + + i = Instant.ofEpochSecond(System.currentTimeMillis() - (1000 * 60 * 60 * 24)); + ZonedDateTime zdt3 = ZonedDateTime.ofInstant(i, ZoneId.of("America/New_York")); + ZonedDateTime zdt4 = ZonedDateTime.ofInstant(i, DateTimeUtils.ZONE_ID_GMT); + assertFalse(DateTimeUtils.equals(zdt3, zdt1)); + assertFalse(DateTimeUtils.equals(zdt4, zdt1)); + } +} diff --git a/src/test/java/io/nats/client/support/EncodingTests.java b/src/test/java/io/nats/client/support/EncodingTests.java index d5e4a185f..fce7467c5 100644 --- a/src/test/java/io/nats/client/support/EncodingTests.java +++ b/src/test/java/io/nats/client/support/EncodingTests.java @@ -17,12 +17,9 @@ import java.util.List; -import static io.nats.client.NKeyUtils.base32Decode; -import static io.nats.client.NKeyUtils.base32Encode; import static io.nats.client.support.Encoding.jsonDecode; import static io.nats.client.support.Encoding.jsonEncode; import static io.nats.client.utils.ResourceUtils.dataAsLines; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; public final class EncodingTests { @@ -70,10 +67,5 @@ private void _testEncodeDecode(String encodedInput, String targetDecode, String else { assertEquals(targetEncode, encoded); } - - byte[] testBytes = decoded.getBytes(); - char[] e32 = base32Encode(testBytes); - byte[] d32 = base32Decode(e32); - assertArrayEquals(testBytes, d32); } } diff --git a/src/test/java/io/nats/client/support/JsonParsingTests.java b/src/test/java/io/nats/client/support/JsonParsingTests.java new file mode 100644 index 000000000..fa0795ff8 --- /dev/null +++ b/src/test/java/io/nats/client/support/JsonParsingTests.java @@ -0,0 +1,950 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.*; + +import static io.nats.client.support.Encoding.jsonEncode; +import static io.nats.client.support.JsonParser.*; +import static io.nats.client.support.JsonParser.Option.KEEP_NULLS; +import static io.nats.client.support.JsonValueUtils.*; +import static io.nats.client.utils.ResourceUtils.dataAsLines; +import static io.nats.client.utils.TestBase.*; +import static org.junit.jupiter.api.Assertions.*; + +public final class JsonParsingTests { + + @Test + public void testStringParsing() { + List<String> encodeds = new ArrayList<>(); + List<String> decodeds = new ArrayList<>(); + Map<String, JsonValue> oMap = new HashMap<>(); + List<JsonValue> list = new ArrayList<>(); + + int x = 0; + addField(key(x++), "b4\\after", oMap, list, encodeds, decodeds); + addField(key(x++), "b4/after", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\"after", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\tafter", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\\bafter", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\\fafter", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\\nafter", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\\rafter", oMap, list, encodeds, decodeds); + addField(key(x++), "b4\\tafter", oMap, list, encodeds, decodeds); + addField(key(x++), "b4" + (char) 0 + "after", oMap, list, encodeds, decodeds); + addField(key(x++), "b4" + (char) 1 + "after", oMap, list, encodeds, decodeds); + + List<String> utfs = dataAsLines("utf8-only-no-ws-test-strings.txt"); + for (String u : utfs) { + String uu = "b4\b\f\n\r\t" + u + "after"; + addField(key(x++), uu, oMap, list, encodeds, decodeds); + } + + addField(key(x++), PLAIN, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_SPACE, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_PRINTABLE, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_DOT, oMap, list, encodeds, decodeds); + addField(key(x++), STAR_NOT_SEGMENT, oMap, list, encodeds, decodeds); + addField(key(x++), GT_NOT_SEGMENT, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_DASH, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_UNDER, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_DOLLAR, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_LOW, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_127, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_FWD_SLASH, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_BACK_SLASH, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_EQUALS, oMap, list, encodeds, decodeds); + addField(key(x++), HAS_TIC, oMap, list, encodeds, decodeds); + + for (int i = 0; i < list.size(); i++) { + JsonValue v = list.get(i); + assertEquals(decodeds.get(i), v.string); + assertEquals(v.toJson(), "\"" + encodeds.get(i) + "\""); + } + } + + private void addField(String name, String decoded, + Map<String, JsonValue> map, List<JsonValue> list, + List<String> encodeds, List<String> decodeds) { + String enc = jsonEncode(decoded); + encodeds.add(enc); + decodeds.add(decoded); + JsonValue jv = new JsonValue(decoded); + map.put(name, jv); + list.add(jv); + } + + @SuppressWarnings("UnnecessaryUnicodeEscape") + @Test + public void testJsonValuePrimitives() throws JsonParseException { + Map<String, JsonValue> oMap = new HashMap<>(); + oMap.put("trueKey1", new JsonValue(true)); + oMap.put("trueKey2", new JsonValue(Boolean.TRUE)); + oMap.put("falseKey1", new JsonValue(false)); + oMap.put("falseKey2", new JsonValue(Boolean.FALSE)); + oMap.put("stringKey", new JsonValue("hello world!")); + oMap.put("escapeStringKey", new JsonValue("h\be\tllo w\u1234orld!")); + oMap.put("nullKey", JsonValue.NULL); + oMap.put("intKey1", new JsonValue(Integer.MAX_VALUE)); + oMap.put("intKey2", new JsonValue(Integer.MIN_VALUE)); + oMap.put("longKey1", new JsonValue(Long.MAX_VALUE)); + oMap.put("longKey2", new JsonValue(Long.MIN_VALUE)); + oMap.put("doubleKey1", new JsonValue(Double.MAX_VALUE)); + oMap.put("doubleKey2", new JsonValue(Double.MIN_VALUE)); + oMap.put("floatKey1", new JsonValue(Float.MAX_VALUE)); + oMap.put("floatKey2", new JsonValue(Float.MIN_VALUE)); + oMap.put("bigDecimalKey1", new JsonValue(new BigDecimal("9223372036854775807.123"))); + oMap.put("bigDecimalKey2", new JsonValue(new BigDecimal("-9223372036854775808.123"))); + oMap.put("bigIntegerKey1", new JsonValue(new BigInteger("9223372036854775807"))); + oMap.put("bigIntegerKey2", new JsonValue(new BigInteger("-9223372036854775808"))); + + // some coverage here + JsonValue vMap = new JsonValue(oMap); + assertEquals(vMap.toJson(), vMap.toString()); + + validateMapTypes(oMap, oMap, true); + + // don't keep nulls + JsonValue parsed = parse(new JsonValue(oMap).toJson()); + assertNotNull(parsed.map); + assertEquals(oMap.size() - 1, parsed.map.size()); + validateMapTypes(parsed.map, oMap, false); + + // keep nulls + parsed = parse(new JsonValue(oMap).toJson(), KEEP_NULLS); + assertNotNull(parsed.map); + assertEquals(oMap.size(), parsed.map.size()); + validateMapTypes(parsed.map, oMap, true); + } + + private static void validateMapTypes(Map<String, JsonValue> map, Map<String, JsonValue> oMap, boolean original) { + assertEquals(JsonValue.Type.BOOL, map.get("trueKey1").type); + assertEquals(JsonValue.Type.BOOL, map.get("trueKey2").type); + assertEquals(JsonValue.Type.BOOL, map.get("falseKey1").type); + assertEquals(JsonValue.Type.BOOL, map.get("falseKey2").type); + assertEquals(JsonValue.Type.STRING, map.get("stringKey").type); + assertEquals(JsonValue.Type.STRING, map.get("escapeStringKey").type); + assertEquals(JsonValue.Type.INTEGER, map.get("intKey1").type); + assertEquals(JsonValue.Type.INTEGER, map.get("intKey2").type); + assertEquals(JsonValue.Type.LONG, map.get("longKey1").type); + assertEquals(JsonValue.Type.LONG, map.get("longKey2").type); + + assertNotNull(map.get("trueKey1").bool); + assertNotNull(map.get("trueKey2").bool); + assertNotNull(map.get("falseKey1").bool); + assertNotNull(map.get("falseKey2").bool); + assertNotNull(map.get("stringKey").string); + assertNotNull(map.get("escapeStringKey").string); + assertNotNull(map.get("intKey1").i); + assertNotNull(map.get("intKey2").i); + assertNotNull(map.get("longKey1").l); + assertNotNull(map.get("longKey2").l); + + assertEquals(oMap.get("trueKey1"), map.get("trueKey1")); + assertEquals(oMap.get("trueKey2"), map.get("trueKey2")); + assertEquals(oMap.get("falseKey1"), map.get("falseKey1")); + assertEquals(oMap.get("falseKey2"), map.get("falseKey2")); + assertEquals(oMap.get("stringKey"), map.get("stringKey")); + assertEquals(oMap.get("escapeStringKey"), map.get("escapeStringKey")); + assertEquals(oMap.get("intKey1"), map.get("intKey1")); + assertEquals(oMap.get("intKey2"), map.get("intKey2")); + assertEquals(oMap.get("longKey1"), map.get("longKey1")); + assertEquals(oMap.get("longKey2"), map.get("longKey2")); + + if (original) { + assertNotNull(oMap.get("intKey1").i); + assertNotNull(oMap.get("intKey2").i); + assertNotNull(oMap.get("longKey1").l); + assertNotNull(oMap.get("longKey2").l); + assertNotNull(oMap.get("doubleKey1").d); + assertNotNull(oMap.get("doubleKey2").d); + assertNotNull(oMap.get("floatKey1").f); + assertNotNull(oMap.get("floatKey2").f); + assertNotNull(oMap.get("bigDecimalKey1").bd); + assertNotNull(oMap.get("bigDecimalKey2").bd); + assertNotNull(oMap.get("bigIntegerKey1").bi); + assertNotNull(oMap.get("bigIntegerKey2").bi); + + assertEquals(JsonValue.Type.NULL, map.get("nullKey").type); + assertNull(map.get("nullKey").object); + assertEquals(oMap.get("nullKey"), map.get("nullKey")); + } + else { + assertNotNull(oMap.get("intKey1").number); + assertNotNull(oMap.get("intKey2").number); + assertNotNull(oMap.get("longKey1").number); + assertNotNull(oMap.get("longKey2").number); + assertNotNull(oMap.get("doubleKey1").number); + assertNotNull(oMap.get("doubleKey2").number); + assertNotNull(oMap.get("floatKey1").number); + assertNotNull(oMap.get("floatKey2").number); + assertNotNull(oMap.get("bigDecimalKey1").number); + assertNotNull(oMap.get("bigDecimalKey2").number); + assertNotNull(oMap.get("bigIntegerKey1").number); + assertNotNull(oMap.get("bigIntegerKey2").number); + } + } + + @Test + public void testArray() throws JsonParseException { + List<JsonValue> list = new ArrayList<>(); + list.add(new JsonValue("string")); + list.add(new JsonValue(true)); + list.add(JsonValue.NULL); + list.add(JsonValue.EMPTY_MAP); + list.add(JsonValue.EMPTY_ARRAY); + + JsonValue root = parse(new JsonValue(list).toJson()); + assertNotNull(root.array); + assertEquals(list.size(), root.array.size()); + List<JsonValue> array = root.array; + for (int i = 0; i < array.size(); i++) { + JsonValue v = array.get(i); + JsonValue p = root.array.get(i); + assertEquals(v.object, p.object); + assertTrue(list.contains(v)); + } + + + list.clear(); + list.add(new JsonValue(1)); + list.add(new JsonValue(Long.MAX_VALUE)); + list.add(new JsonValue(Double.MAX_VALUE)); + list.add(new JsonValue(Float.MAX_VALUE)); + list.add(new JsonValue(new BigDecimal(Double.toString(Double.MAX_VALUE)))); + list.add(new JsonValue(new BigInteger(Long.toString(Long.MAX_VALUE)))); + + root = parse(new JsonValue(list).toJson()); + assertNotNull(root.array); + assertEquals(list.size(), root.array.size()); + array = root.array; + for (int i = 0; i < array.size(); i++) { + JsonValue v = array.get(i); + JsonValue p = root.array.get(i); + assertEquals(v.object, p.object); + assertEquals(v.number, p.number); + } + + Map<String, JsonValue> rootMap = new HashMap<>(); + rootMap.put("list", new JsonValue(list)); + rootMap.put("array", new JsonValue(list.toArray(new JsonValue[0]))); + root = new JsonValue(rootMap); + List<JsonValue> mappedList = readValue(root, "list").array; + + List<JsonValue> mappedList2 = parse(new JsonValue(mappedList).toJson()).array; + List<JsonValue> mappedArray = readValue(root, "array").array; + List<JsonValue> mappedArray2 = parse(new JsonValue(list.toArray(new JsonValue[0])).toJson()).array; + for (int i = 0; i < list.size(); i++) { + JsonValue v = list.get(i); + JsonValue lv = mappedList.get(i); + JsonValue lv2 = mappedList2.get(i); + JsonValue av = mappedArray.get(i); + JsonValue av2 = mappedArray2.get(i); + assertNotNull(lv); + assertNotNull(lv2); + assertNotNull(av); + assertNotNull(av2); + assertEquals(v, lv); + assertEquals(v, av); + + // conversions are not perfect for doubles and floats, but that's a java thing, not a parser thing + if (v.type == lv2.type) { + assertEquals(v, lv2); + } + if (v.type == av2.type) { + assertEquals(v, av2); + } + } + } + + @Test + public void testListReading() { + List<JsonValue> jvList = new ArrayList<>(); + jvList.add(new JsonValue("string1")); + jvList.add(new JsonValue("string2")); + jvList.add(new JsonValue("")); + jvList.add(new JsonValue(true)); + jvList.add(new JsonValue((String)null)); + jvList.add(JsonValue.NULL); + jvList.add(JsonValue.EMPTY_MAP); + jvList.add(JsonValue.EMPTY_ARRAY); + jvList.add(new JsonValue(Integer.MAX_VALUE)); + jvList.add(new JsonValue(Long.MAX_VALUE)); + Map<String, JsonValue> jvMap = new HashMap<>(); + jvMap.put("list", new JsonValue(jvList)); + JsonValue root = new JsonValue(jvMap); + + List<String> list = readStringList(root, "list"); + assertEquals(3, list.size()); + assertTrue(list.contains("string1")); + assertTrue(list.contains("string2")); + assertTrue(list.contains("")); + + list = readStringListIgnoreEmpty(root, "list"); + assertEquals(2, list.size()); + assertTrue(list.contains("string1")); + assertTrue(list.contains("string2")); + + jvList.remove(0); + jvList.remove(0); + jvList.remove(0); + list = readOptionalStringList(root, "list"); + assertNull(list); + + list = readOptionalStringList(root, "na"); + assertNull(list); + + jvList.clear(); + Duration d0 = Duration.ofNanos(10000000000L); + Duration d1 = Duration.ofNanos(20000000000L); + Duration d2 = Duration.ofNanos(30000000000L); + + jvList.add(instance(d0)); + jvList.add(instance(d1)); + jvList.add(instance(d2)); + jvList.add(new JsonValue("not duration nanos")); + + root = new JsonValue(jvMap); + + List<Duration> dlist = readNanosList(root, "list"); + assertEquals(3, dlist.size()); + assertEquals(d0, dlist.get(0)); + assertEquals(d1, dlist.get(1)); + assertEquals(d2, dlist.get(2)); + } + + @Test + public void testGetIntLong() { + JsonValue i = new JsonValue(Integer.MAX_VALUE); + JsonValue li = new JsonValue((long)Integer.MAX_VALUE); + JsonValue lmax = new JsonValue(Long.MAX_VALUE); + JsonValue lmin = new JsonValue(Long.MIN_VALUE); + assertEquals(Integer.MAX_VALUE, getInteger(i)); + assertEquals(Integer.MAX_VALUE, getInteger(li)); + assertNull(getInteger(lmax)); + assertNull(getInteger(lmin)); + assertNull(getInteger(JsonValue.NULL)); + assertNull(getInteger(JsonValue.EMPTY_MAP)); + assertNull(getInteger(JsonValue.EMPTY_ARRAY)); + + assertEquals(Integer.MAX_VALUE, getLong(i)); + assertEquals(Integer.MAX_VALUE, getLong(li)); + assertEquals(Long.MAX_VALUE, getLong(lmax)); + assertEquals(Long.MIN_VALUE, getLong(lmin)); + assertNull(getLong(JsonValue.NULL)); + assertNull(getLong(JsonValue.EMPTY_MAP)); + assertNull(getLong(JsonValue.EMPTY_ARRAY)); + + assertEquals(Integer.MAX_VALUE, getLong(i, -1)); + assertEquals(Integer.MAX_VALUE, getLong(li, -1)); + assertEquals(Long.MAX_VALUE, getLong(lmax, -1)); + assertEquals(Long.MIN_VALUE, getLong(lmin, -1)); + assertEquals(-1, getLong(JsonValue.NULL, -1)); + assertEquals(-1, getLong(JsonValue.EMPTY_MAP, -1)); + assertEquals(-1, getLong(JsonValue.EMPTY_ARRAY, -1)); + } + + @Test + public void testConstantsAreReadOnly() { + assertThrows(UnsupportedOperationException.class, () -> JsonValue.EMPTY_MAP.map.put("foo", null)); + assertThrows(UnsupportedOperationException.class, () -> JsonValue.EMPTY_ARRAY.array.add(null)); + } + + @Test + public void testNullJsonValue() { + assertEquals(JsonValue.Type.NULL, JsonValue.NULL.type); + assertNull(JsonValue.NULL.object); + assertNull(JsonValue.NULL.map); + assertNull(JsonValue.NULL.array); + assertNull(JsonValue.NULL.string); + assertNull(JsonValue.NULL.bool); + assertNull(JsonValue.NULL.number); + assertNull(JsonValue.NULL.i); + assertNull(JsonValue.NULL.l); + assertNull(JsonValue.NULL.d); + assertNull(JsonValue.NULL.f); + assertNull(JsonValue.NULL.bd); + assertNull(JsonValue.NULL.bi); + assertEquals(JsonValue.NULL, new JsonValue((String)null)); + assertEquals(JsonValue.NULL, new JsonValue((Boolean) null)); + assertEquals(JsonValue.NULL, new JsonValue((Map<String, JsonValue>)null)); + assertEquals(JsonValue.NULL, new JsonValue((List<JsonValue>)null)); + assertEquals(JsonValue.NULL, new JsonValue((JsonValue[])null)); + assertEquals(JsonValue.NULL, new JsonValue((BigDecimal)null)); + assertEquals(JsonValue.NULL, new JsonValue((BigInteger) null)); + } + + @Test + public void testGetMapped() { + ZonedDateTime zdt = DateTimeUtils.gmtNow(); + Duration dur = Duration.ofNanos(4273); + Duration dur2 = Duration.ofNanos(7342); + + JsonValue v = new JsonValue(new HashMap<>()); + v.map.put("bool", new JsonValue(Boolean.TRUE)); + v.map.put("string", new JsonValue("hello")); + v.map.put("int", new JsonValue(Integer.MAX_VALUE)); + v.map.put("long", new JsonValue(Long.MAX_VALUE)); + v.map.put("date", new JsonValue(DateTimeUtils.toRfc3339(zdt))); + v.map.put("dur", new JsonValue(dur.toNanos())); + v.map.put("strings", new JsonValue(new JsonValue[]{new JsonValue("s1"), new JsonValue("s2")})); + v.map.put("durs", new JsonValue(new JsonValue[]{new JsonValue(dur.toNanos()), new JsonValue(dur2.toNanos())})); + + assertNotNull(readValue(v, "string")); + assertNull(readValue(v, "na")); + assertEquals(JsonValue.EMPTY_MAP, readObject(v, "na")); + assertNull(read(null, "na", vv -> vv)); + assertNull(read(JsonValue.NULL, "na", vv -> vv)); + assertNull(read(JsonValue.EMPTY_MAP, "na", vv -> vv)); + + assertNull(readDate(null, "na")); + assertNull(readDate(JsonValue.NULL, "na")); + assertNull(readDate(JsonValue.EMPTY_MAP, "na")); + assertEquals(zdt, readDate(v, "date")); + assertNull(readDate(v, "int")); + + assertFalse(readBoolean(null, "na")); + assertFalse(readBoolean(null, "na", false)); + assertTrue(readBoolean(null, "na", true)); + assertFalse(readBoolean(JsonValue.NULL, "na")); + assertFalse(readBoolean(JsonValue.NULL, "na", false)); + assertTrue(readBoolean(JsonValue.NULL, "na", true)); + assertFalse(readBoolean(JsonValue.EMPTY_MAP, "na")); + assertFalse(readBoolean(JsonValue.EMPTY_MAP, "na", false)); + assertTrue(readBoolean(JsonValue.EMPTY_MAP, "na", true)); + assertFalse(readBoolean(v, "na")); + assertFalse(readBoolean(v, "na", false)); + assertTrue(readBoolean(v, "na", true)); + assertFalse(readBoolean(v, "int")); + assertFalse(readBoolean(v, "int", false)); + assertTrue(readBoolean(v, "int", true)); + + assertTrue(readBoolean(v, "bool")); + assertTrue(readBoolean(v, "bool", false)); + assertFalse(readBoolean(v, "na")); + assertFalse(readBoolean(v, "na", false)); + assertTrue(readBoolean(v, "na", true)); + + assertEquals("hello", readString(v, "string")); + assertEquals("hello", readString(v, "string", null)); + assertNull(readString(v, "na")); + assertNull(readString(v, "na", null)); + assertEquals("default", readString(v, "na", "default")); + assertNull(readString(JsonValue.NULL, "na")); + assertNull(readString(JsonValue.NULL, "na", null)); + assertEquals("default", readString(JsonValue.NULL, "na", "default")); + + assertEquals(zdt, readDate(v, "date")); + assertNull(readDate(v, "na")); + assertThrows(DateTimeException.class, () -> readDate(v, "string")); + + assertEquals(Integer.MAX_VALUE, readInteger(v, "int")); + assertEquals(Integer.MAX_VALUE, readInteger(v, "int", -1)); + assertNull(readInteger(v, "string")); + assertEquals(-1, readInteger(v, "string", -1)); + assertNull(readInteger(v, "na")); + assertEquals(-1, readInteger(v, "na", -1)); + + assertEquals(Long.MAX_VALUE, readLong(v, "long")); + assertEquals(Long.MAX_VALUE, readLong(v, "long", -1)); + assertNull(readLong(v, "string")); + assertEquals(-1, readLong(v, "string", -1)); + assertNull(readLong(v, "na")); + assertEquals(-1, readLong(v, "na", -1)); + + assertEquals(dur, readNanos(v, "dur")); + assertEquals(dur, readNanos(v, "dur", null)); + assertNull(readNanos(v, "string")); + assertNull(readNanos(v, "string", null)); + assertEquals(dur2, readNanos(v, "string", dur2)); + assertNull(readNanos(v, "na")); + assertNull(readNanos(v, "na", null)); + assertEquals(dur2, readNanos(v, "na", dur2)); + + // these aren't maps + JsonValue jvn = new JsonValue(1); + JsonValue jvs = new JsonValue("s"); + JsonValue jvb = new JsonValue(true); + JsonValue[] notMaps = new JsonValue[] {JsonValue.NULL, JsonValue.EMPTY_ARRAY, jvn, jvs, jvb}; + + for (JsonValue vv : notMaps) { + assertNull(readValue(vv, "na")); + assertEquals(JsonValue.EMPTY_MAP, readObject(vv, "na")); + assertNull(readDate(vv, "na")); + assertNull(readInteger(vv, "na")); + assertEquals(-1, readInteger(vv, "na", -1)); + assertNull(readLong(vv, "na")); + assertEquals(-2, readLong(vv, "na", -2)); + assertFalse(readBoolean(vv, "na")); + assertNull(readBoolean(vv, "na", null)); + assertTrue(readBoolean(vv, "na", true)); + assertFalse(readBoolean(vv, "na", false)); + assertNull(readNanos(vv, "na")); + assertEquals(Duration.ZERO, readNanos(vv, "na", Duration.ZERO)); + } + } + + @Test + public void equalsContract() { + Map<String, JsonValue> map1 = new HashMap<>(); + map1.put("1", new JsonValue(1)); + Map<String, JsonValue> map2 = new HashMap<>(); + map1.put("2", new JsonValue(2)); + List<JsonValue> list3 = new ArrayList<>(); + list3.add(new JsonValue(3)); + List<JsonValue> list4 = new ArrayList<>(); + list4.add(new JsonValue(4)); + EqualsVerifier.simple().forClass(JsonValue.class) + .withPrefabValues(Map.class, map1, map2) + .withPrefabValues(List.class, list3, list4) + .withIgnoredFields("object", "number", "mapOrder") + .suppress(Warning.BIGDECIMAL_EQUALITY) + .verify(); + } + + private void validateParse(JsonValue expected, String json) throws JsonParseException { + char[] ca = json.toCharArray(); + byte[] ba = json.getBytes(); + + assertEquals(expected, parse(json)); + assertEquals(expected, parse(json, 0)); + assertEquals(expected, parse(json, KEEP_NULLS)); + assertEquals(expected, parse(ca)); + assertEquals(expected, parse(ca, 0)); + assertEquals(expected, parse(ca, KEEP_NULLS)); + assertEquals(expected, parse(ba)); + assertEquals(expected, parse(ba, KEEP_NULLS)); + + assertEquals(expected, parseUnchecked(json)); + assertEquals(expected, parseUnchecked(json, 0)); + assertEquals(expected, parseUnchecked(json, KEEP_NULLS)); + assertEquals(expected, parseUnchecked(ca)); + assertEquals(expected, parseUnchecked(ca, 0)); + assertEquals(expected, parseUnchecked(ca, KEEP_NULLS)); + assertEquals(expected, parseUnchecked(ba)); + assertEquals(expected, parseUnchecked(ba, KEEP_NULLS)); + } + + @Test + public void testParsingCoverage() throws JsonParseException { + validateParse(JsonValue.NULL, ""); + validateParse(JsonValue.EMPTY_MAP, "{}"); + validateParse(JsonValue.EMPTY_ARRAY, "[]"); + + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> parse("{}", -1)); + assertTrue(iae.getMessage().contains("Invalid start index.")); + + validateThrows("{", "Text must end with '}'"); + validateThrows("{{", "Cannot directly nest another Object or Array."); + validateThrows("{[", "Cannot directly nest another Object or Array."); + validateThrows("{\"foo\":1 ]", "Expected a ',' or '}'."); + validateThrows("{\"foo\" 1", "Expected a ':' after a key."); + validateThrows("[\"bad\",", "Unexpected end of data."); // missing close + validateThrows("[1Z]", "Invalid value."); + validateThrows("t", "Invalid value."); + validateThrows("f", "Invalid value."); + validateThrows("\"u", "Unterminated string."); + validateThrows("\"u\r", "Unterminated string."); + validateThrows("\"u\n", "Unterminated string."); + validateThrows("\"\\x\"", "Illegal escape."); + validateThrows("\"\\u000", "Illegal escape."); + validateThrows("\"\\uzzzz", "Illegal escape."); + + JsonValue v = JsonParser.parse((char[])null); + assertEquals(JsonValue.NULL, v); + + v = parse("{\"foo\":1,}"); + assertEquals(1, v.map.size()); + assertTrue(v.map.containsKey("foo")); + assertEquals(1, v.map.get("foo").i); + + v = parse("INFO{\"foo\":1,}", 4); + assertEquals(1, v.map.size()); + assertTrue(v.map.containsKey("foo")); + assertEquals(1, v.map.get("foo").i); + + v = parse("[\"foo\",]"); // handles dangling commas fine + assertEquals(1, v.array.size()); + assertEquals("foo", v.array.get(0).string); + + String s = "foo \b \t \n \f \r \" \\ /"; + String j = "\"" + Encoding.jsonEncode(s) + "\""; + v = parse(j); + assertNotNull(v.string); + assertEquals(s, v.string); + + // every constructor + String json = "{}"; + new JsonParser(json.toCharArray()); + new JsonParser(json.toCharArray(), Option.KEEP_NULLS); + parse(json.toCharArray()); + parse(json.toCharArray(), 0); + parse(json.toCharArray(), Option.KEEP_NULLS); + parse(json.toCharArray(), 0, Option.KEEP_NULLS); + parse(json); + parse(json, 0); + parse(json, Option.KEEP_NULLS); + parse(json, 0, Option.KEEP_NULLS); + parse(json.getBytes()); + parse(json.getBytes(), Option.KEEP_NULLS); + parseUnchecked(json.toCharArray()); + parseUnchecked(json.toCharArray(), 0); + parseUnchecked(json.toCharArray(), Option.KEEP_NULLS); + parseUnchecked(json.toCharArray(), 0, Option.KEEP_NULLS); + parseUnchecked(json); + parseUnchecked(json, 0); + parseUnchecked(json, Option.KEEP_NULLS); + parseUnchecked(json, 0, Option.KEEP_NULLS); + parseUnchecked(json.getBytes()); + parseUnchecked(json.getBytes(), Option.KEEP_NULLS); + } + + private void validateThrows(String json, String errorText) { + // also provides coverage for every constructor + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json.toCharArray()))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json.toCharArray(), 0))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json.toCharArray(), Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json.toCharArray(), 0, Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json, 0))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json, Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json, 0, Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json.getBytes()))); + validateThrowError(errorText, assertThrows(JsonParseException.class, () -> parse(json.getBytes(), Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json.toCharArray()))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json.toCharArray(), 0))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json.toCharArray(), Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json.toCharArray(), 0, Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json, 0))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json, Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json, 0, Option.KEEP_NULLS))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json.getBytes()))); + validateThrowError(errorText, assertThrows(RuntimeException.class, () -> parseUnchecked(json.getBytes(), Option.KEEP_NULLS))); + } + + private static void validateThrowError(String errorText, Exception e) { + assertTrue(e.getMessage().contains(errorText)); + } + + @Test + public void testNumberParsing() throws JsonParseException { + assertEquals(JsonValue.Type.INTEGER, parse("1").type); + assertEquals(JsonValue.Type.INTEGER, parse(Integer.toString(Integer.MAX_VALUE)).type); + assertEquals(JsonValue.Type.INTEGER, parse(Integer.toString(Integer.MIN_VALUE)).type); + assertEquals(JsonValue.Type.LONG, parse(Long.toString((long)Integer.MAX_VALUE + 1)).type); + assertEquals(JsonValue.Type.LONG, parse(Long.toString((long)Integer.MIN_VALUE - 1)).type); + assertEquals(JsonValue.Type.DOUBLE, parse("-0").type); + assertEquals(JsonValue.Type.DOUBLE, parse("-0.0").type); + assertEquals(JsonValue.Type.DOUBLE, parse("0.1d").type); + assertEquals(JsonValue.Type.DOUBLE, parse("0.f").type); + assertEquals(JsonValue.Type.DOUBLE, parse("0.1f").type); + assertEquals(JsonValue.Type.DOUBLE, parse("-0x1.fffp1").type); + assertEquals(JsonValue.Type.DOUBLE, parse("0x1.0P-1074").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("0.2").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("244273.456789012345").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("244273.456789012345").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("0.1234567890123456789").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("-24.42e7345").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("-24.42E7345").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("-.01").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, parse("00.001").type); + assertEquals(JsonValue.Type.BIG_INTEGER, parse("12345678901234567890").type); + + String str = new BigInteger( Long.toString(Long.MAX_VALUE) ).add( BigInteger.ONE ).toString(); + assertEquals(JsonValue.Type.BIG_INTEGER, parse(str).type); + + validateThrows("-0x123", "Invalid value."); + JsonParseException e; + + e = assertThrows(JsonParseException.class, () -> parse("-")); + assertTrue(e.getMessage().contains("Invalid value.")); + + e = assertThrows(JsonParseException.class, () -> parse("00")); + assertTrue(e.getMessage().contains("Invalid value.")); + + e = assertThrows(JsonParseException.class, () -> parse("NaN")); + assertTrue(e.getMessage().contains("Invalid value.")); + + e = assertThrows(JsonParseException.class, () -> parse("-NaN")); + assertTrue(e.getMessage().contains("Invalid value.")); + + e = assertThrows(JsonParseException.class, () -> parse("Infinity")); + assertTrue(e.getMessage().contains("Invalid value.")); + + e = assertThrows(JsonParseException.class, () -> parse("-Infinity")); + assertTrue(e.getMessage().contains("Invalid value.")); + } + + @Test + public void testValueUtilsInstanceDuration() { + JsonValue v = instance(Duration.ofSeconds(1)); + assertNotNull(v.l); + assertEquals(1000000000L, v.l); + } + + static class TestSerializableMap implements JsonSerializable { + @Override + public String toJson() { + JsonValue v = new JsonValue(new HashMap<>()); + v.map.put("a", new JsonValue("A")); + v.map.put("b", new JsonValue("B")); + v.map.put("c", new JsonValue("C")); + return v.toJson(); + } + } + + static class TestSerializableList implements JsonSerializable { + @Override + public String toJson() { + JsonValue v = new JsonValue(new ArrayList<>()); + v.array.add(new JsonValue("X")); + v.array.add(new JsonValue("Y")); + v.array.add(new JsonValue("Z")); + return v.toJson(); + } + } + + @Test + public void testValueUtilsInstanceList() { + List<Object> list = new ArrayList<>(); + list.add("Hello"); + list.add(""); + list.add('c'); + list.add(1); + list.add(1L); + list.add(1D); + list.add(1F); + list.add(new BigDecimal("1.0")); + list.add(new BigInteger("1")); + list.add(true); + list.add(new HashMap<>()); + list.add(new ArrayList<>()); + list.add(new HashSet<>()); + list.add(new TestSerializableMap()); + list.add(new TestSerializableList()); + list.add(null); + JsonValue v = instance(list); + assertNotNull(v.array); + assertEquals(16, v.array.size()); + assertEquals(JsonValue.Type.STRING, v.array.get(0).type); + assertEquals(JsonValue.Type.NULL, v.array.get(1).type); + assertEquals(JsonValue.Type.STRING, v.array.get(2).type); + assertEquals(JsonValue.Type.INTEGER, v.array.get(3).type); + assertEquals(JsonValue.Type.LONG, v.array.get(4).type); + assertEquals(JsonValue.Type.DOUBLE, v.array.get(5).type); + assertEquals(JsonValue.Type.FLOAT, v.array.get(6).type); + assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(7).type); + assertEquals(JsonValue.Type.BIG_INTEGER, v.array.get(8).type); + assertEquals(JsonValue.Type.BOOL, v.array.get(9).type); + assertEquals(JsonValue.Type.MAP, v.array.get(10).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(11).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(12).type); + assertEquals(JsonValue.Type.MAP, v.array.get(13).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(14).type); + assertEquals(JsonValue.Type.NULL, v.array.get(15).type); + } + + @Test + public void testValueUtilsInstanceMap() { + Map<Object, Object> map = new HashMap<>(); + map.put("string", "Hello"); + map.put("char", 'c'); + map.put("int", 1); + map.put("long", Long.MAX_VALUE); + map.put("double", 1D); + map.put("float", 1F); + map.put("bd", new BigDecimal("1.0")); + map.put("bi", new BigInteger(Long.toString(Long.MAX_VALUE))); + map.put("bool", true); + map.put("map", new HashMap<>()); + map.put("list", new ArrayList<>()); + map.put("set", new HashSet<>()); + map.put("smap", new TestSerializableMap()); + map.put("slist", new TestSerializableList()); + map.put("jv", JsonValue.EMPTY_MAP); + map.put("null", null); + map.put("jvNull", JsonValue.NULL); + map.put("empty_is_null", ""); + validateMap(true, false, instance(map)); + } + + @Test + public void testValueUtilsMapBuilder() { + MapBuilder builder = mapBuilder() + .put("string", "Hello") + .put("char", 'c') + .put("int", 1) + .put("long", Long.MAX_VALUE) + .put("double", 1D) + .put("float", 1F) + .put("bd", new BigDecimal("1.0")) + .put("bi", new BigInteger(Long.toString(Long.MAX_VALUE))) + .put("bool", true) + .put("map", new HashMap<>()) + .put("list", new ArrayList<>()) + .put("set", new HashSet<>()) + .put("smap", new TestSerializableMap()) + .put("slist", new TestSerializableList()) + .put("jv", JsonValue.EMPTY_MAP) + .put("null", null) + .put("jvNull", JsonValue.NULL) + .put("empty_is_null", ""); + validateMap(false, false, builder.toJsonValue()); + //noinspection deprecation + validateMap(false, false, builder.getJsonValue()); // coverage for deprecated + validateMap(false, true, JsonParser.parseUnchecked(builder.toJson())); + } + + private static void validateMap(boolean checkNull, boolean parsed, JsonValue v) { + assertNotNull(v.map); + assertEquals(JsonValue.Type.STRING, v.map.get("string").type); + assertEquals(JsonValue.Type.STRING, v.map.get("char").type); + assertEquals(JsonValue.Type.INTEGER, v.map.get("int").type); + assertEquals(JsonValue.Type.LONG, v.map.get("long").type); + if (parsed) { + assertEquals(JsonValue.Type.BIG_DECIMAL, v.map.get("double").type); + assertEquals(JsonValue.Type.BIG_DECIMAL, v.map.get("float").type); + assertEquals(JsonValue.Type.LONG, v.map.get("bi").type); + } + else { + assertEquals(JsonValue.Type.DOUBLE, v.map.get("double").type); + assertEquals(JsonValue.Type.FLOAT, v.map.get("float").type); + assertEquals(JsonValue.Type.BIG_INTEGER, v.map.get("bi").type); + } + assertEquals(JsonValue.Type.BIG_DECIMAL, v.map.get("bd").type); + assertEquals(JsonValue.Type.BOOL, v.map.get("bool").type); + assertEquals(JsonValue.Type.MAP, v.map.get("map").type); + assertEquals(JsonValue.Type.ARRAY, v.map.get("list").type); + assertEquals(JsonValue.Type.ARRAY, v.map.get("set").type); + assertEquals(JsonValue.Type.MAP, v.map.get("smap").type); + assertEquals(JsonValue.Type.ARRAY, v.map.get("slist").type); + assertEquals(JsonValue.Type.MAP, v.map.get("jv").type); + if (checkNull) { + assertEquals(18, v.map.size()); + assertEquals(JsonValue.Type.NULL, v.map.get("null").type); + assertEquals(JsonValue.Type.NULL, v.map.get("jvNull").type); + assertEquals(JsonValue.Type.NULL, v.map.get("empty_is_null").type); + } + else { + assertEquals(15, v.map.size()); + } + } + + @Test + public void testValueUtilsInstanceArray() { + List<Object> list = new ArrayList<>(); + list.add("Hello"); + list.add('c'); + list.add(1); + list.add(Long.MAX_VALUE); + list.add(1D); + list.add(1F); + list.add(new BigDecimal("1.0")); + list.add(new BigInteger(Long.toString(Long.MAX_VALUE))); + list.add(true); + list.add(new HashMap<>()); + list.add(new ArrayList<>()); + list.add(new TestSerializableMap()); + list.add(new TestSerializableList()); + list.add(JsonValue.EMPTY_MAP); + list.add(null); + list.add(JsonValue.NULL); + validateArray(true, false, instance(list)); + } + + @Test + public void testValueUtilsArrayBuilder() { + ArrayBuilder builder = arrayBuilder() + .add("Hello") + .add('c') + .add(1) + .add(Long.MAX_VALUE) + .add(1D) + .add(1F) + .add(new BigDecimal("1.0")) + .add(new BigInteger(Long.toString(Long.MAX_VALUE))) + .add(true) + .add(new HashMap<>()) + .add(new ArrayList<>()) + .add(new TestSerializableMap()) + .add(new TestSerializableList()) + .add(JsonValue.EMPTY_MAP) + .add(null) + .add(JsonValue.NULL); + validateArray(false, false, builder.toJsonValue()); + //noinspection deprecation + validateArray(false, false, builder.getJsonValue()); // coverage for deprecated + validateArray(false, true, JsonParser.parseUnchecked(builder.toJson())); + } + + private static void validateArray(boolean checkNull, boolean parsed, JsonValue v) { + assertNotNull(v.array); + assertEquals(JsonValue.Type.STRING, v.array.get(0).type); + assertEquals(JsonValue.Type.STRING, v.array.get(1).type); + assertEquals(JsonValue.Type.INTEGER, v.array.get(2).type); + assertEquals(JsonValue.Type.LONG, v.array.get(3).type); + if (parsed) { + assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(4).type); + assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(5).type); + assertEquals(JsonValue.Type.LONG, v.array.get(7).type); + } + else { + assertEquals(JsonValue.Type.DOUBLE, v.array.get(4).type); + assertEquals(JsonValue.Type.FLOAT, v.array.get(5).type); + assertEquals(JsonValue.Type.BIG_INTEGER, v.array.get(7).type); + } + assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(6).type); + assertEquals(JsonValue.Type.BOOL, v.array.get(8).type); + assertEquals(JsonValue.Type.MAP, v.array.get(9).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(10).type); + assertEquals(JsonValue.Type.MAP, v.array.get(11).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(12).type); + assertEquals(JsonValue.Type.MAP, v.array.get(13).type); + if (checkNull) { + assertEquals(16, v.array.size()); + assertEquals(JsonValue.Type.NULL, v.array.get(14).type); + assertEquals(JsonValue.Type.NULL, v.array.get(15).type); + } + else { + assertEquals(14, v.array.size()); + } + } + + @Test + public void testReadStringStringMap() { + JsonValue jv = mapBuilder() + .put("stringString", mapBuilder().put("a", "A").put("b", "B").toJsonValue()) + .put("empty", new HashMap<>()) + .put("string", "string") + .toJsonValue(); + + assertNull(readStringStringMap(jv, "string")); + assertNull(readStringStringMap(jv, "empty")); + Map<String, String> stringString = readStringStringMap(jv, "stringString"); + assertNotNull(stringString); + assertEquals(2, stringString.size()); + assertEquals("A", stringString.get("a")); + assertEquals("B", stringString.get("b")); + } +} diff --git a/src/test/java/io/nats/client/support/JwtUtilsTests.java b/src/test/java/io/nats/client/support/JwtUtilsTests.java index a33eb035a..32ca9114d 100644 --- a/src/test/java/io/nats/client/support/JwtUtilsTests.java +++ b/src/test/java/io/nats/client/support/JwtUtilsTests.java @@ -322,14 +322,11 @@ public void testUserClaimJson() { assertEquals(FULL_JSON, uc.toJson()); } - @SuppressWarnings("deprecation") @Test public void testMiscCoverage() { long seconds = JwtUtils.currentTimeSeconds(); sleep(1000); assertTrue(JwtUtils.currentTimeSeconds() > seconds); - // coverage, also makes sure that the deprecated JsonUtils.NATS_USER_JWT_FORMAT is pointed properly - assertEquals(io.nats.jwt.JwtUtils.NATS_USER_JWT_FORMAT, NATS_USER_JWT_FORMAT); } private static final String BASIC_JSON = "{\"issuer_account\":\"test-issuer-account\",\"type\":\"user\",\"version\":2,\"subs\":-1,\"data\":-1,\"payload\":-1}"; diff --git a/src/test/java/io/nats/service/ServiceTests.java b/src/test/java/io/nats/service/ServiceTests.java index 0ff4b267c..b487817e7 100644 --- a/src/test/java/io/nats/service/ServiceTests.java +++ b/src/test/java/io/nats/service/ServiceTests.java @@ -18,7 +18,10 @@ import io.nats.client.impl.JetStreamTestBase; import io.nats.client.impl.MockNatsConnection; import io.nats.client.impl.NatsMessage; -import io.nats.client.support.*; +import io.nats.client.support.DateTimeUtils; +import io.nats.client.support.JsonSerializable; +import io.nats.client.support.JsonUtils; +import io.nats.client.support.JsonValue; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; @@ -33,9 +36,9 @@ import java.util.function.Supplier; import static io.nats.client.impl.NatsPackageScopeWorkarounds.getDispatchers; +import static io.nats.client.support.JsonUtils.toKey; import static io.nats.client.support.JsonValueUtils.readInteger; import static io.nats.client.support.JsonValueUtils.readString; -import static io.nats.client.support.JsonWriteUtils.toKey; import static io.nats.client.support.NatsConstants.DOT; import static io.nats.client.support.NatsConstants.EMPTY; import static io.nats.service.Service.SRV_PING; @@ -728,7 +731,7 @@ public void testEndpointConstruction() { .build(); assertEquals(NAME, e.getName()); assertEquals(SUBJECT, e.getSubject()); - assertTrue(Validator.mapEquals(metadata, e.getMetadata())); + assertTrue(JsonUtils.mapEquals(metadata, e.getMetadata())); // some subject testing e = new Endpoint(NAME, "foo.>"); @@ -740,7 +743,7 @@ public void testEndpointConstruction() { e = new Endpoint(NAME, SUBJECT, metadata); assertEquals(NAME, e.getName()); assertEquals(SUBJECT, e.getSubject()); - assertTrue(Validator.mapEquals(metadata, e.getMetadata())); + assertTrue(JsonUtils.mapEquals(metadata, e.getMetadata())); assertThrows(IllegalArgumentException.class, () -> Endpoint.builder().build()); // many names are bad and is required @@ -947,7 +950,7 @@ public void testServiceEndpointConstruction() { .endpointMetadata(metadata) .handler(smh) .build(); - assertTrue(Validator.mapEquals(metadata, se.getMetadata())); + assertTrue(JsonUtils.mapEquals(metadata, se.getMetadata())); IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> ServiceEndpoint.builder().build()); @@ -1152,7 +1155,7 @@ public TestStatsData(JsonValue jv) { @Override public String toJson() { - return JsonWriteUtils.toKey(getClass()) + toJsonValue().toJson(); + return JsonUtils.toKey(getClass()) + toJsonValue().toJson(); } @Override