From ed1ee79aef4a326564605d568f96e466c6d2766b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 15 Sep 2025 15:19:58 +0200 Subject: [PATCH] Share JSON library consistently This commit shades the use of 'com.vaadin.external.google:android-json' in the three modules that use it. The configuration processor already did that and this commit does the same for configuration-metadata and the CLI. As a result of this commit, 'android-json' is not used nor managed internally. Closes gh-45504 --- cli/spring-boot-cli/build.gradle | 13 +- .../src/json-shade/README.adoc | 5 + .../springframework/boot/cli/json/JSON.java | 124 +++ .../boot/cli/json/JSONArray.java | 669 ++++++++++++++ .../boot/cli/json/JSONException.java | 49 + .../boot/cli/json/JSONObject.java | 836 ++++++++++++++++++ .../boot/cli/json/JSONStringer.java | 429 +++++++++ .../boot/cli/json/JSONTokener.java | 555 ++++++++++++ .../cli/command/init/InitializrService.java | 4 +- .../init/InitializrServiceMetadata.java | 6 +- .../init/InitializrServiceMetadataTests.java | 4 +- .../init/ProjectGenerationRequestTests.java | 4 +- config/checkstyle/checkstyle-suppressions.xml | 2 + .../build.gradle | 10 +- .../src/json-shade/README.adoc | 5 + .../boot/configurationmetadata/json/JSON.java | 124 +++ .../configurationmetadata/json/JSONArray.java | 669 ++++++++++++++ .../json/JSONException.java | 49 + .../json/JSONObject.java | 836 ++++++++++++++++++ .../json/JSONStringer.java | 429 +++++++++ .../json/JSONTokener.java | 555 ++++++++++++ .../configurationmetadata/JsonReader.java | 4 +- .../JsonReaderTests.java | 3 +- .../build.gradle | 7 - 24 files changed, 5369 insertions(+), 22 deletions(-) create mode 100644 cli/spring-boot-cli/src/json-shade/README.adoc create mode 100644 cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSON.java create mode 100644 cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONArray.java create mode 100644 cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONException.java create mode 100644 cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONObject.java create mode 100644 cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONStringer.java create mode 100644 cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONTokener.java create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/README.adoc create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSON.java create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONArray.java create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONException.java create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONObject.java create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONStringer.java create mode 100644 configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONTokener.java diff --git a/cli/spring-boot-cli/build.gradle b/cli/spring-boot-cli/build.gradle index de248614913..1667fbfeef5 100644 --- a/cli/spring-boot-cli/build.gradle +++ b/cli/spring-boot-cli/build.gradle @@ -26,6 +26,14 @@ plugins { description = "Spring Boot CLI" +sourceSets { + main { + java { + srcDir file("src/json-shade/java") + } + } +} + configurations { loader testRepository @@ -37,7 +45,6 @@ dependencies { compileOnlyProject(project(":core:spring-boot")) implementation(project(":loader:spring-boot-loader-tools")) - implementation("com.vaadin.external.google:android-json") implementation("jline:jline") implementation("net.sf.jopt-simple:jopt-simple") implementation("org.apache.httpcomponents.client5:httpclient5") @@ -54,6 +61,10 @@ dependencies { testImplementation(project(":test-support:spring-boot-test-support")) } +architectureCheck { + nullMarked = false +} + tasks.register("fullJar", Jar) { dependsOn configurations.loader archiveClassifier = "full" diff --git a/cli/spring-boot-cli/src/json-shade/README.adoc b/cli/spring-boot-cli/src/json-shade/README.adoc new file mode 100644 index 00000000000..65480069457 --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/README.adoc @@ -0,0 +1,5 @@ +## Shaded JSON + +This source was originally taken from `com.vaadin.external.google:android-json` which +provides a clean room re-implementation of the `org.json` APIs and does not include the +"Do not use for evil" clause. diff --git a/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSON.java b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSON.java new file mode 100644 index 00000000000..8c276d88d3b --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSON.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.cli.json; + +class JSON { + + static double checkDouble(double d) throws JSONException { + if (Double.isInfinite(d) || Double.isNaN(d)) { + throw new JSONException("Forbidden numeric value: " + d); + } + return d; + } + + static Boolean toBoolean(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String stringValue) { + if ("true".equalsIgnoreCase(stringValue)) { + return true; + } + if ("false".equalsIgnoreCase(stringValue)) { + return false; + } + } + return null; + } + + static Double toDouble(Object value) { + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + if (value instanceof String) { + try { + return Double.valueOf((String) value); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return null; + } + + static Integer toInteger(Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof String) { + try { + return (int) Double.parseDouble((String) value); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return null; + } + + static Long toLong(Object value) { + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + try { + return (long) Double.parseDouble((String) value); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return null; + } + + static String toString(Object value) { + if (value instanceof String) { + return (String) value; + } + if (value != null) { + return String.valueOf(value); + } + return null; + } + + public static JSONException typeMismatch(Object indexOrName, Object actual, String requiredType) + throws JSONException { + if (actual == null) { + throw new JSONException("Value at " + indexOrName + " is null."); + } + throw new JSONException("Value " + actual + " at " + indexOrName + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); + } + + public static JSONException typeMismatch(Object actual, String requiredType) throws JSONException { + if (actual == null) { + throw new JSONException("Value is null."); + } + throw new JSONException("Value " + actual + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); + } + +} diff --git a/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONArray.java b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONArray.java new file mode 100644 index 00000000000..2185272dfa6 --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONArray.java @@ -0,0 +1,669 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.cli.json; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * A dense indexed sequence of values. Values may be any mix of {@link JSONObject + * JSONObjects}, other {@link JSONArray JSONArrays}, Strings, Booleans, Integers, Longs, + * Doubles, {@code null} or {@link JSONObject#NULL}. Values may not be + * {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not + * listed here. + *

+ * {@code JSONArray} has the same type coercion behavior and optional/mandatory accessors + * as {@link JSONObject}. See that class' documentation for details. + *

+ * Warning: this class represents null in two incompatible ways: the + * standard Java {@code null} reference, and the sentinel value {@link JSONObject#NULL}. + * In particular, {@code get} fails if the requested index holds the null reference, but + * succeeds if it holds {@code JSONObject.NULL}. + *

+ * Instances of this class are not thread safe. Although this class is nonfinal, it was + * not designed for inheritance and should not be subclassed. In particular, self-use by + * overridable methods is not specified. See Effective Java Item 17, "Design and + * Document or inheritance or else prohibit it" for further information. + */ +public class JSONArray { + + private final List values; + + /** + * Creates a {@code JSONArray} with no values. + */ + public JSONArray() { + this.values = new ArrayList<>(); + } + + /** + * Creates a new {@code JSONArray} by copying all values from the given collection. + * @param copyFrom a collection whose values are of supported types. Unsupported + * values are not permitted and will yield an array in an inconsistent state. + */ + /* Accept a raw type for API compatibility */ + @SuppressWarnings("rawtypes") + public JSONArray(Collection copyFrom) { + this(); + if (copyFrom != null) { + for (Iterator it = copyFrom.iterator(); it.hasNext();) { + put(JSONObject.wrap(it.next())); + } + } + } + + /** + * Creates a new {@code JSONArray} with values from the next array in the tokener. + * @param readFrom a tokener whose nextValue() method will yield a {@code JSONArray}. + * @throws JSONException if the parse fails or doesn't yield a {@code JSONArray}. + * @throws JSONException if processing of json failed + */ + public JSONArray(JSONTokener readFrom) throws JSONException { + /* + * Getting the parser to populate this could get tricky. Instead, just parse to + * temporary JSONArray and then steal the data from that. + */ + Object object = readFrom.nextValue(); + if (object instanceof JSONArray) { + this.values = ((JSONArray) object).values; + } + else { + throw JSON.typeMismatch(object, "JSONArray"); + } + } + + /** + * Creates a new {@code JSONArray} with values from the JSON string. + * @param json a JSON-encoded string containing an array. + * @throws JSONException if the parse fails or doesn't yield a {@code + * JSONArray}. + */ + public JSONArray(String json) throws JSONException { + this(new JSONTokener(json)); + } + + /** + * Creates a new {@code JSONArray} with values from the given primitive array. + * @param array a primitive array + * @throws JSONException if processing of json failed + */ + public JSONArray(Object array) throws JSONException { + if (!array.getClass().isArray()) { + throw new JSONException("Not a primitive array: " + array.getClass()); + } + final int length = Array.getLength(array); + this.values = new ArrayList<>(length); + for (int i = 0; i < length; ++i) { + put(JSONObject.wrap(Array.get(array, i))); + } + } + + /** + * Returns the number of values in this array. + * @return the length of this array + */ + public int length() { + return this.values.size(); + } + + /** + * Appends {@code value} to the end of this array. + * @param value the value + * @return this array. + */ + public JSONArray put(boolean value) { + this.values.add(value); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(double value) throws JSONException { + this.values.add(JSON.checkDouble(value)); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value the value + * @return this array. + */ + public JSONArray put(int value) { + this.values.add(value); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value the value + * @return this array. + */ + public JSONArray put(long value) { + this.values.add(value); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be + * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. Unsupported + * values are not permitted and will cause the array to be in an inconsistent state. + * @return this array. + */ + public JSONArray put(Object value) { + this.values.add(value); + return this; + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value the value + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, boolean value) throws JSONException { + return put(index, (Boolean) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, double value) throws JSONException { + return put(index, (Double) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value the value + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, int value) throws JSONException { + return put(index, (Integer) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value the value + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, long value) throws JSONException { + return put(index, (Long) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be + * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, Object value) throws JSONException { + if (value instanceof Number) { + // deviate from the original by checking all Numbers, not just floats & + // doubles + JSON.checkDouble(((Number) value).doubleValue()); + } + while (this.values.size() <= index) { + this.values.add(null); + } + this.values.set(index, value); + return this; + } + + /** + * Returns true if this array has no value at {@code index}, or if its value is the + * {@code null} reference or {@link JSONObject#NULL}. + * @param index the index to set the value to + * @return true if this array has no value at {@code index} + */ + public boolean isNull(int index) { + Object value = opt(index); + return value == null || value == JSONObject.NULL; + } + + /** + * Returns the value at {@code index}. + * @param index the index to get the value from + * @return the value at {@code index}. + * @throws JSONException if this array has no value at {@code index}, or if that value + * is the {@code null} reference. This method returns normally if the value is + * {@code JSONObject#NULL}. + */ + public Object get(int index) throws JSONException { + try { + Object value = this.values.get(index); + if (value == null) { + throw new JSONException("Value at " + index + " is null."); + } + return value; + } + catch (IndexOutOfBoundsException e) { + throw new JSONException("Index " + index + " out of range [0.." + this.values.size() + ")"); + } + } + + /** + * Returns the value at {@code index}, or null if the array has no value at + * {@code index}. + * @param index the index to get the value from + * @return the value at {@code index} or {@code null} + */ + public Object opt(int index) { + if (index < 0 || index >= this.values.size()) { + return null; + } + return this.values.get(index); + } + + /** + * Removes and returns the value at {@code index}, or null if the array has no value + * at {@code index}. + * @param index the index of the value to remove + * @return the previous value at {@code index} + */ + public Object remove(int index) { + if (index < 0 || index >= this.values.size()) { + return null; + } + return this.values.remove(index); + } + + /** + * Returns the value at {@code index} if it exists and is a boolean or can be coerced + * to a boolean. + * @param index the index to get the value from + * @return the value at {@code index} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to a boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = get(index); + Boolean result = JSON.toBoolean(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "boolean"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is a boolean or can be coerced + * to a boolean. Returns false otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code false} + */ + public boolean optBoolean(int index) { + return optBoolean(index, false); + } + + /** + * Returns the value at {@code index} if it exists and is a boolean or can be coerced + * to a boolean. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public boolean optBoolean(int index, boolean fallback) { + Object object = opt(index); + Boolean result = JSON.toBoolean(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is a double or can be coerced + * to a double. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to a double. + */ + public double getDouble(int index) throws JSONException { + Object object = get(index); + Double result = JSON.toDouble(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "double"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is a double or can be coerced + * to a double. Returns {@code NaN} otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code NaN} + */ + public double optDouble(int index) { + return optDouble(index, Double.NaN); + } + + /** + * Returns the value at {@code index} if it exists and is a double or can be coerced + * to a double. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public double optDouble(int index, double fallback) { + Object object = opt(index); + Double result = JSON.toDouble(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is an int or can be coerced to + * an int. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to an int. + */ + public int getInt(int index) throws JSONException { + Object object = get(index); + Integer result = JSON.toInteger(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "int"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is an int or can be coerced to + * an int. Returns 0 otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code 0} + */ + public int optInt(int index) { + return optInt(index, 0); + } + + /** + * Returns the value at {@code index} if it exists and is an int or can be coerced to + * an int. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public int optInt(int index, int fallback) { + Object object = opt(index); + Integer result = JSON.toInteger(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is a long or can be coerced to + * a long. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to a long. + */ + public long getLong(int index) throws JSONException { + Object object = get(index); + Long result = JSON.toLong(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "long"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is a long or can be coerced to + * a long. Returns 0 otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code 0} + */ + public long optLong(int index) { + return optLong(index, 0L); + } + + /** + * Returns the value at {@code index} if it exists and is a long or can be coerced to + * a long. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public long optLong(int index, long fallback) { + Object object = opt(index); + Long result = JSON.toLong(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists, coercing it if necessary. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if no such value exists. + */ + public String getString(int index) throws JSONException { + Object object = get(index); + String result = JSON.toString(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "String"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists, coercing it if necessary. Returns + * the empty string if no such value exists. + * @param index the index to get the value from + * @return the {@code value} or an empty string + */ + public String optString(int index) { + return optString(index, ""); + } + + /** + * Returns the value at {@code index} if it exists, coercing it if necessary. Returns + * {@code fallback} if no such value exists. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public String optString(int index, String fallback) { + Object object = opt(index); + String result = JSON.toString(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONArray}. + * @param index the index to get the value from + * @return the array at {@code index} + * @throws JSONException if the value doesn't exist or is not a {@code + * JSONArray}. + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + else { + throw JSON.typeMismatch(index, object, "JSONArray"); + } + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONArray}. Returns null otherwise. + * @param index the index to get the value from + * @return the array at {@code index} or {@code null} + */ + public JSONArray optJSONArray(int index) { + Object object = opt(index); + return object instanceof JSONArray ? (JSONArray) object : null; + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONObject}. + * @param index the index to get the value from + * @return the object at {@code index} + * @throws JSONException if the value doesn't exist or is not a {@code + * JSONObject}. + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + else { + throw JSON.typeMismatch(index, object, "JSONObject"); + } + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONObject}. Returns null otherwise. + * @param index the index to get the value from + * @return the object at {@code index} or {@code null} + */ + public JSONObject optJSONObject(int index) { + Object object = opt(index); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Returns a new object whose values are the values in this array, and whose names are + * the values in {@code names}. Names and values are paired up by index from 0 through + * to the shorter array's length. Names that are not strings will be coerced to + * strings. This method returns null if either array is empty. + * @param names the property names + * @return a json object + * @throws JSONException if processing of json failed + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + JSONObject result = new JSONObject(); + int length = Math.min(names.length(), this.values.size()); + if (length == 0) { + return null; + } + for (int i = 0; i < length; i++) { + String name = JSON.toString(names.opt(i)); + result.put(name, opt(i)); + } + return result; + } + + /** + * Returns a new string by alternating this array's values with {@code + * separator}. This array's string values are quoted and have their special characters + * escaped. For example, the array containing the strings '12" pizza', 'taco' and + * 'soda' joined on '+' returns this:
"12\" pizza"+"taco"+"soda"
+ * @param separator the separator to use + * @return the joined value + * @throws JSONException if processing of json failed + */ + public String join(String separator) throws JSONException { + JSONStringer stringer = new JSONStringer(); + stringer.open(JSONStringer.Scope.NULL, ""); + for (int i = 0, size = this.values.size(); i < size; i++) { + if (i > 0) { + stringer.out.append(separator); + } + stringer.value(this.values.get(i)); + } + stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); + return stringer.out.toString(); + } + + /** + * Encodes this array as a compact JSON string, such as:
[94043,90210]
+ * @return a compact JSON string representation of this array + */ + @Override + public String toString() { + try { + JSONStringer stringer = new JSONStringer(); + writeTo(stringer); + return stringer.toString(); + } + catch (JSONException e) { + return null; + } + } + + /** + * Encodes this array as a human-readable JSON string for debugging, such as:
+	 * [
+	 *     94043,
+	 *     90210
+	 * ]
+ * @param indentSpaces the number of spaces to indent for each level of nesting. + * @return a human-readable JSON string of this array + * @throws JSONException if processing of json failed + */ + public String toString(int indentSpaces) throws JSONException { + JSONStringer stringer = new JSONStringer(indentSpaces); + writeTo(stringer); + return stringer.toString(); + } + + void writeTo(JSONStringer stringer) throws JSONException { + stringer.array(); + for (Object value : this.values) { + stringer.value(value); + } + stringer.endArray(); + } + + @Override + public boolean equals(Object o) { + return o instanceof JSONArray && ((JSONArray) o).values.equals(this.values); + } + + @Override + public int hashCode() { + // diverge from the original, which doesn't implement hashCode + return this.values.hashCode(); + } + +} diff --git a/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONException.java b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONException.java new file mode 100644 index 00000000000..ebde92bce3d --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.cli.json; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * Thrown to indicate a problem with the JSON API. Such problems include: + *
    + *
  • Attempts to parse or construct malformed documents + *
  • Use of null as a name + *
  • Use of numeric types not available to JSON, such as {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + *
  • Lookups using an out of range index or nonexistent name + *
  • Type mismatches on lookups + *
+ *

+ * Although this is a checked exception, it is rarely recoverable. Most callers should + * simply wrap this exception in an unchecked exception and rethrow:

+ *     public JSONArray toJSONObject() {
+ *     try {
+ *         JSONObject result = new JSONObject();
+ *         ...
+ *     } catch (JSONException e) {
+ *         throw new RuntimeException(e);
+ *     }
+ * }
+ */ +public class JSONException extends Exception { + + public JSONException(String s) { + super(s); + } + +} diff --git a/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONObject.java b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONObject.java new file mode 100644 index 00000000000..a68468d32a0 --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONObject.java @@ -0,0 +1,836 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.cli.json; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * A modifiable set of name/value mappings. Names are unique, non-null strings. Values may + * be any mix of {@link JSONObject JSONObjects}, {@link JSONArray JSONArrays}, Strings, + * Booleans, Integers, Longs, Doubles or {@link #NULL}. Values may not be {@code null}, + * {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not + * listed here. + *

+ * This class can coerce values to another type when requested. + *

+ *

+ * This class can look up both mandatory and optional values: + *

    + *
  • Use getType() to retrieve a mandatory value. This fails with a + * {@code JSONException} if the requested name has no value or if the value cannot be + * coerced to the requested type. + *
  • Use optType() to retrieve an optional value. This returns a + * system- or user-supplied default if the requested name has no value or if the value + * cannot be coerced to the requested type. + *
+ *

+ * Warning: this class represents null in two incompatible ways: the + * standard Java {@code null} reference, and the sentinel value {@link JSONObject#NULL}. + * In particular, calling {@code put(name, null)} removes the named entry from the object + * but {@code put(name, JSONObject.NULL)} stores an entry whose value is + * {@code JSONObject.NULL}. + *

+ * Instances of this class are not thread safe. Although this class is nonfinal, it was + * not designed for inheritance and should not be subclassed. In particular, self-use by + * overrideable methods is not specified. See Effective Java Item 17, "Design and + * Document or inheritance or else prohibit it" for further information. + */ +public class JSONObject { + + private static final Double NEGATIVE_ZERO = -0d; + + /** + * A sentinel value used to explicitly define a name with no value. Unlike + * {@code null}, names with this value: + *

    + *
  • show up in the {@link #names} array + *
  • show up in the {@link #keys} iterator + *
  • return {@code true} for {@link #has(String)} + *
  • do not throw on {@link #get(String)} + *
  • are included in the encoded JSON string. + *
+ *

+ * This value violates the general contract of {@link Object#equals} by returning true + * when compared to {@code null}. Its {@link #toString} method returns "null". + */ + public static final Object NULL = new Object() { + + @Override + public boolean equals(Object o) { + return o == this || o == null; // API specifies this broken equals + // implementation + } + + @Override + public String toString() { + return "null"; + } + + }; + + private final Map nameValuePairs; + + /** + * Creates a {@code JSONObject} with no name/value mappings. + */ + public JSONObject() { + this.nameValuePairs = new LinkedHashMap<>(); + } + + /** + * Creates a new {@code JSONObject} by copying all name/value mappings from the given + * map. + * @param copyFrom a map whose keys are of type {@link String} and whose values are of + * supported types. + * @throws NullPointerException if any of the map's keys are null. + */ + /* (accept a raw type for API compatibility) */ + @SuppressWarnings("rawtypes") + public JSONObject(Map copyFrom) { + this(); + Map contentsTyped = copyFrom; + for (Map.Entry entry : contentsTyped.entrySet()) { + /* + * Deviate from the original by checking that keys are non-null and of the + * proper type. (We still defer validating the values). + */ + String key = (String) entry.getKey(); + if (key == null) { + throw new NullPointerException("key == null"); + } + this.nameValuePairs.put(key, wrap(entry.getValue())); + } + } + + /** + * Creates a new {@code JSONObject} with name/value mappings from the next object in + * the tokener. + * @param readFrom a tokener whose nextValue() method will yield a {@code JSONObject}. + * @throws JSONException if the parse fails or doesn't yield a {@code JSONObject}. + */ + public JSONObject(JSONTokener readFrom) throws JSONException { + /* + * Getting the parser to populate this could get tricky. Instead, just parse to + * temporary JSONObject and then steal the data from that. + */ + Object object = readFrom.nextValue(); + if (object instanceof JSONObject) { + this.nameValuePairs = ((JSONObject) object).nameValuePairs; + } + else { + throw JSON.typeMismatch(object, "JSONObject"); + } + } + + /** + * Creates a new {@code JSONObject} with name/value mappings from the JSON string. + * @param json a JSON-encoded string containing an object. + * @throws JSONException if the parse fails or doesn't yield a {@code + * JSONObject}. + */ + public JSONObject(String json) throws JSONException { + this(new JSONTokener(json)); + } + + /** + * Creates a new {@code JSONObject} by copying mappings for the listed names from the + * given object. Names that aren't present in {@code copyFrom} will be skipped. + * @param copyFrom the source + * @param names the property names + * @throws JSONException if an error occurs + */ + public JSONObject(JSONObject copyFrom, String[] names) throws JSONException { + this(); + for (String name : names) { + Object value = copyFrom.opt(name); + if (value != null) { + this.nameValuePairs.put(name, value); + } + } + } + + /** + * Returns the number of name/value mappings in this object. + * @return the number of name/value mappings in this object + */ + public int length() { + return this.nameValuePairs.size(); + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, boolean value) throws JSONException { + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, double value) throws JSONException { + this.nameValuePairs.put(checkName(name), JSON.checkDouble(value)); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, int value) throws JSONException { + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, long value) throws JSONException { + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. If the value is {@code null}, any existing mapping for {@code name} + * is removed. + * @param name the name of the property + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link #NULL}, or {@code null}. May not be {@link Double#isNaN() + * NaNs} or {@link Double#isInfinite() infinities}. + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, Object value) throws JSONException { + if (value == null) { + this.nameValuePairs.remove(name); + return this; + } + if (value instanceof Number) { + // deviate from the original by checking all Numbers, not just floats & + // doubles + JSON.checkDouble(((Number) value).doubleValue()); + } + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Equivalent to {@code put(name, value)} when both parameters are non-null; does + * nothing otherwise. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject putOpt(String name, Object value) throws JSONException { + if (name == null || value == null) { + return this; + } + return put(name, value); + } + + /** + * Appends {@code value} to the array already mapped to {@code name}. If this object + * has no mapping for {@code name}, this inserts a new mapping. If the mapping exists + * but its value is not an array, the existing and new values are inserted in order + * into a new array which is itself mapped to {@code name}. In aggregate, this allows + * values to be added to a mapping one at a time. + * @param name the name of the property + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link #NULL} or null. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject accumulate(String name, Object value) throws JSONException { + Object current = this.nameValuePairs.get(checkName(name)); + if (current == null) { + return put(name, value); + } + + // check in accumulate, since array.put(Object) doesn't do any checking + if (value instanceof Number) { + JSON.checkDouble(((Number) value).doubleValue()); + } + + if (current instanceof JSONArray array) { + array.put(value); + } + else { + JSONArray array = new JSONArray(); + array.put(current); + array.put(value); + this.nameValuePairs.put(name, array); + } + return this; + } + + String checkName(String name) throws JSONException { + if (name == null) { + throw new JSONException("Names must be non-null"); + } + return name; + } + + /** + * Removes the named mapping if it exists; does nothing otherwise. + * @param name the name of the property + * @return the value previously mapped by {@code name}, or null if there was no such + * mapping. + */ + public Object remove(String name) { + return this.nameValuePairs.remove(name); + } + + /** + * Returns true if this object has no mapping for {@code name} or if it has a mapping + * whose value is {@link #NULL}. + * @param name the name of the property + * @return true if this object has no mapping for {@code name} + */ + public boolean isNull(String name) { + Object value = this.nameValuePairs.get(name); + return value == null || value == NULL; + } + + /** + * Returns true if this object has a mapping for {@code name}. The mapping may be + * {@link #NULL}. + * @param name the name of the property + * @return true if this object has a mapping for {@code name} + */ + public boolean has(String name) { + return this.nameValuePairs.containsKey(name); + } + + /** + * Returns the value mapped by {@code name}. + * @param name the name of the property + * @return the value + * @throws JSONException if no such mapping exists. + */ + public Object get(String name) throws JSONException { + Object result = this.nameValuePairs.get(name); + if (result == null) { + throw new JSONException("No value for " + name); + } + return result; + } + + /** + * Returns the value mapped by {@code name}, or null if no such mapping exists. + * @param name the name of the property + * @return the value or {@code null} + */ + public Object opt(String name) { + return this.nameValuePairs.get(name); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a boolean or can be + * coerced to a boolean. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to a + * boolean. + */ + public boolean getBoolean(String name) throws JSONException { + Object object = get(name); + Boolean result = JSON.toBoolean(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "boolean"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a boolean or can be + * coerced to a boolean. Returns false otherwise. + * @param name the name of the property + * @return the value or {@code null} + */ + public boolean optBoolean(String name) { + return optBoolean(name, false); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a boolean or can be + * coerced to a boolean. Returns {@code fallback} otherwise. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public boolean optBoolean(String name, boolean fallback) { + Object object = opt(name); + Boolean result = JSON.toBoolean(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a double or can be + * coerced to a double. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to a + * double. + */ + public double getDouble(String name) throws JSONException { + Object object = get(name); + Double result = JSON.toDouble(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "double"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a double or can be + * coerced to a double. Returns {@code NaN} otherwise. + * @param name the name of the property + * @return the value or {@code NaN} + */ + public double optDouble(String name) { + return optDouble(name, Double.NaN); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a double or can be + * coerced to a double. Returns {@code fallback} otherwise. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public double optDouble(String name, double fallback) { + Object object = opt(name); + Double result = JSON.toDouble(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is an int or can be + * coerced to an int. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to an int. + */ + public int getInt(String name) throws JSONException { + Object object = get(name); + Integer result = JSON.toInteger(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "int"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is an int or can be + * coerced to an int. Returns 0 otherwise. + * @param name the name of the property + * @return the value of {@code 0} + */ + public int optInt(String name) { + return optInt(name, 0); + } + + /** + * Returns the value mapped by {@code name} if it exists and is an int or can be + * coerced to an int. Returns {@code fallback} otherwise. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public int optInt(String name, int fallback) { + Object object = opt(name); + Integer result = JSON.toInteger(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a long or can be + * coerced to a long. Note that JSON represents numbers as doubles, so this is + * lossy; use strings to transfer numbers over JSON. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to a long. + */ + public long getLong(String name) throws JSONException { + Object object = get(name); + Long result = JSON.toLong(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "long"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a long or can be + * coerced to a long. Returns 0 otherwise. Note that JSON represents numbers as + * doubles, so this is lossy; use strings to transfer numbers via + * JSON. + * @param name the name of the property + * @return the value or {@code 0L} + */ + public long optLong(String name) { + return optLong(name, 0L); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a long or can be + * coerced to a long. Returns {@code fallback} otherwise. Note that JSON represents + * numbers as doubles, so this is lossy; use strings to transfer + * numbers over JSON. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public long optLong(String name, long fallback) { + Object object = opt(name); + Long result = JSON.toLong(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists, coercing it if necessary. + * @param name the name of the property + * @return the value + * @throws JSONException if no such mapping exists. + */ + public String getString(String name) throws JSONException { + Object object = get(name); + String result = JSON.toString(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "String"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists, coercing it if necessary. + * Returns the empty string if no such mapping exists. + * @param name the name of the property + * @return the value or an empty string + */ + public String optString(String name) { + return optString(name, ""); + } + + /** + * Returns the value mapped by {@code name} if it exists, coercing it if necessary. + * Returns {@code fallback} if no such mapping exists. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public String optString(String name, String fallback) { + Object object = opt(name); + String result = JSON.toString(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONArray}. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or is not a {@code + * JSONArray}. + */ + public JSONArray getJSONArray(String name) throws JSONException { + Object object = get(name); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + else { + throw JSON.typeMismatch(name, object, "JSONArray"); + } + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONArray}. Returns null otherwise. + * @param name the name of the property + * @return the value or {@code null} + */ + public JSONArray optJSONArray(String name) { + Object object = opt(name); + return object instanceof JSONArray ? (JSONArray) object : null; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONObject}. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or is not a {@code + * JSONObject}. + */ + public JSONObject getJSONObject(String name) throws JSONException { + Object object = get(name); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + else { + throw JSON.typeMismatch(name, object, "JSONObject"); + } + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONObject}. Returns null otherwise. + * @param name the name of the property + * @return the value or {@code null} + */ + public JSONObject optJSONObject(String name) { + Object object = opt(name); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Returns an array with the values corresponding to {@code names}. The array contains + * null for names that aren't mapped. This method returns null if {@code names} is + * either null or empty. + * @param names the names of the properties + * @return the array + */ + public JSONArray toJSONArray(JSONArray names) { + JSONArray result = new JSONArray(); + if (names == null) { + return null; + } + int length = names.length(); + if (length == 0) { + return null; + } + for (int i = 0; i < length; i++) { + String name = JSON.toString(names.opt(i)); + result.put(opt(name)); + } + return result; + } + + /** + * Returns an iterator of the {@code String} names in this object. The returned + * iterator supports {@link Iterator#remove() remove}, which will remove the + * corresponding mapping from this object. If this object is modified after the + * iterator is returned, the iterator's behavior is undefined. The order of the keys + * is undefined. + * @return the keys + */ + /* Return a raw type for API compatibility */ + @SuppressWarnings("rawtypes") + public Iterator keys() { + return this.nameValuePairs.keySet().iterator(); + } + + /** + * Returns an array containing the string names in this object. This method returns + * null if this object contains no mappings. + * @return the array + */ + public JSONArray names() { + return this.nameValuePairs.isEmpty() ? null : new JSONArray(new ArrayList<>(this.nameValuePairs.keySet())); + } + + /** + * Encodes this object as a compact JSON string, such as: + *

{"query":"Pizza","locations":[94043,90210]}
+ * @return a string representation of the object. + */ + @Override + public String toString() { + try { + JSONStringer stringer = new JSONStringer(); + writeTo(stringer); + return stringer.toString(); + } + catch (JSONException e) { + return null; + } + } + + /** + * Encodes this object as a human-readable JSON string for debugging, such as:
+	 * {
+	 *     "query": "Pizza",
+	 *     "locations": [
+	 *         94043,
+	 *         90210
+	 *     ]
+	 * }
+ * @param indentSpaces the number of spaces to indent for each level of nesting. + * @return a string representation of the object. + * @throws JSONException if an error occurs + */ + public String toString(int indentSpaces) throws JSONException { + JSONStringer stringer = new JSONStringer(indentSpaces); + writeTo(stringer); + return stringer.toString(); + } + + void writeTo(JSONStringer stringer) throws JSONException { + stringer.object(); + for (Map.Entry entry : this.nameValuePairs.entrySet()) { + stringer.key(entry.getKey()).value(entry.getValue()); + } + stringer.endObject(); + } + + /** + * Encodes the number as a JSON string. + * @param number a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return the encoded value + * @throws JSONException if an error occurs + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Number must be non-null"); + } + + double doubleValue = number.doubleValue(); + JSON.checkDouble(doubleValue); + + // the original returns "-0" instead of "-0.0" for negative zero + if (number.equals(NEGATIVE_ZERO)) { + return "-0"; + } + + long longValue = number.longValue(); + if (doubleValue == longValue) { + return Long.toString(longValue); + } + + return number.toString(); + } + + /** + * Encodes {@code data} as a JSON string. This applies quotes and any necessary + * character escaping. + * @param data the string to encode. Null will be interpreted as an empty string. + * @return the quoted value + */ + public static String quote(String data) { + if (data == null) { + return "\"\""; + } + try { + JSONStringer stringer = new JSONStringer(); + stringer.open(JSONStringer.Scope.NULL, ""); + stringer.value(data); + stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); + return stringer.toString(); + } + catch (JSONException e) { + throw new AssertionError(); + } + } + + /** + * Wraps the given object if necessary. + *

+ * If the object is null or, returns {@link #NULL}. If the object is a + * {@code JSONArray} or {@code JSONObject}, no wrapping is necessary. If the object is + * {@code NULL}, no wrapping is necessary. If the object is an array or + * {@code Collection}, returns an equivalent {@code JSONArray}. If the object is a + * {@code Map}, returns an equivalent {@code JSONObject}. If the object is a primitive + * wrapper type or {@code String}, returns the object. Otherwise if the object is from + * a {@code java} package, returns the result of {@code toString}. If wrapping fails, + * returns null. + * @param o the object to wrap + * @return the wrapped object + */ + @SuppressWarnings("rawtypes") + public static Object wrap(Object o) { + if (o == null) { + return NULL; + } + if (o instanceof JSONArray || o instanceof JSONObject) { + return o; + } + if (o.equals(NULL)) { + return o; + } + try { + if (o instanceof Collection) { + return new JSONArray((Collection) o); + } + else if (o.getClass().isArray()) { + return new JSONArray(o); + } + if (o instanceof Map) { + return new JSONObject((Map) o); + } + if (o instanceof Boolean || o instanceof Byte || o instanceof Character || o instanceof Double + || o instanceof Float || o instanceof Integer || o instanceof Long || o instanceof Short + || o instanceof String) { + return o; + } + if (o.getClass().getPackage().getName().startsWith("java.")) { + return o.toString(); + } + } + catch (Exception ex) { + // Ignore + } + return null; + } + +} diff --git a/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONStringer.java b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONStringer.java new file mode 100644 index 00000000000..3a9a8736a68 --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONStringer.java @@ -0,0 +1,429 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.cli.json; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most application + * developers should use those methods directly and disregard this API. For example:

+ * JSONObject object = ...
+ * String json = object.toString();
+ *

+ * Stringers only encode well-formed JSON strings. In particular: + *

    + *
  • The stringer must have exactly one top-level array or object. + *
  • Lexical scopes must be balanced: every call to {@link #array} must have a matching + * call to {@link #endArray} and every call to {@link #object} must have a matching call + * to {@link #endObject}. + *
  • Arrays may not contain keys (property names). + *
  • Objects must alternate keys (property names) and values. + *
  • Values are inserted with either literal {@link #value(Object) value} calls, or by + * nesting arrays or objects. + *
+ * Calls that would result in a malformed JSON string will fail with a + * {@link JSONException}. + *

+ * This class provides no facility for pretty-printing (ie. indenting) output. To encode + * indented output, use {@link JSONObject#toString(int)} or + * {@link JSONArray#toString(int)}. + *

+ * Some implementations of the API support at most 20 levels of nesting. Attempts to + * create more than 20 levels of nesting may fail with a {@link JSONException}. + *

+ * Each stringer may be used to encode a single top level value. Instances of this class + * are not thread safe. Although this class is nonfinal, it was not designed for + * inheritance and should not be subclassed. In particular, self-use by overrideable + * methods is not specified. See Effective Java Item 17, "Design and Document or + * inheritance or else prohibit it" for further information. + */ +public class JSONStringer { + + /** + * The output data, containing at most one top-level array or object. + */ + final StringBuilder out = new StringBuilder(); + + /** + * Lexical scoping elements within this stringer, necessary to insert the appropriate + * separator characters (i.e. commas and colons) and to detect nesting errors. + */ + enum Scope { + + /** + * An array with no elements requires no separators or newlines before it is + * closed. + */ + EMPTY_ARRAY, + + /** + * An array with at least one value requires a comma and newline before the next + * element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no keys or values requires no separators or newlines before it + * is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must be a value. + */ + DANGLING_KEY, + + /** + * An object with at least one name/value pair requires a comma and newline before + * the next element. + */ + NONEMPTY_OBJECT, + + /** + * A special bracketless array needed by JSONStringer.join() and + * JSONObject.quote() only. Not used for JSON encoding. + */ + NULL + + } + + /** + * Unlike the original implementation, this stack isn't limited to 20 levels of + * nesting. + */ + private final List stack = new ArrayList<>(); + + /** + * A string containing a full set of spaces for a single level of indentation, or null + * for no pretty printing. + */ + private final String indent; + + public JSONStringer() { + this.indent = null; + } + + JSONStringer(int indentSpaces) { + char[] indentChars = new char[indentSpaces]; + Arrays.fill(indentChars, ' '); + this.indent = new String(indentChars); + } + + /** + * Begins encoding a new array. Each call to this method must be paired with a call to + * {@link #endArray}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer array() throws JSONException { + return open(Scope.EMPTY_ARRAY, "["); + } + + /** + * Ends encoding the current array. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer endArray() throws JSONException { + return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]"); + } + + /** + * Begins encoding a new object. Each call to this method must be paired with a call + * to {@link #endObject}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer object() throws JSONException { + return open(Scope.EMPTY_OBJECT, "{"); + } + + /** + * Ends encoding the current object. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer endObject() throws JSONException { + return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given bracket. + * @param empty any necessary whitespace + * @param openBracket the open bracket + * @return this object + * @throws JSONException if processing of json failed + */ + JSONStringer open(Scope empty, String openBracket) throws JSONException { + if (this.stack.isEmpty() && !this.out.isEmpty()) { + throw new JSONException("Nesting problem: multiple top-level roots"); + } + beforeValue(); + this.stack.add(empty); + this.out.append(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the given + * bracket. + * @param empty any necessary whitespace + * @param nonempty the current scope + * @param closeBracket the close bracket + * @return the JSON stringer + * @throws JSONException if processing of json failed + */ + JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException { + Scope context = peek(); + if (context != nonempty && context != empty) { + throw new JSONException("Nesting problem"); + } + + this.stack.remove(this.stack.size() - 1); + if (context == nonempty) { + newline(); + } + this.out.append(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + * @return the scope + * @throws JSONException if processing of json failed + */ + private Scope peek() throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + return this.stack.get(this.stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + * @param topOfStack the scope at the top of the stack + */ + private void replaceTop(Scope topOfStack) { + this.stack.set(this.stack.size() - 1, topOfStack); + } + + /** + * Encodes {@code value}. + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double or null. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(Object value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + + if (value instanceof JSONArray) { + ((JSONArray) value).writeTo(this); + return this; + } + else if (value instanceof JSONObject) { + ((JSONObject) value).writeTo(this); + return this; + } + + beforeValue(); + + if (value == null || value instanceof Boolean || value == JSONObject.NULL) { + this.out.append(value); + + } + else if (value instanceof Number) { + this.out.append(JSONObject.numberToString((Number) value)); + + } + else { + string(value.toString()); + } + + return this; + } + + /** + * Encodes {@code value} to this stringer. + * @param value the value to encode + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(boolean value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + this.out.append(value); + return this; + } + + /** + * Encodes {@code value} to this stringer. + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(double value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + this.out.append(JSONObject.numberToString(value)); + return this; + } + + /** + * Encodes {@code value} to this stringer. + * @param value the value to encode + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(long value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + this.out.append(value); + return this; + } + + private void string(String value) { + this.out.append("\""); + for (int i = 0, length = value.length(); i < length; i++) { + char c = value.charAt(i); + + /* + * From RFC 4627, "All Unicode characters may be placed within the quotation + * marks except for the characters that must be escaped: quotation mark, + * reverse solidus, and the control characters (U+0000 through U+001F)." + */ + switch (c) { + case '"', '\\', '/' -> this.out.append('\\').append(c); + case '\t' -> this.out.append("\\t"); + case '\b' -> this.out.append("\\b"); + case '\n' -> this.out.append("\\n"); + case '\r' -> this.out.append("\\r"); + case '\f' -> this.out.append("\\f"); + default -> { + if (c <= 0x1F) { + this.out.append(String.format("\\u%04x", (int) c)); + } + else { + this.out.append(c); + } + } + } + + } + this.out.append("\""); + } + + private void newline() { + if (this.indent == null) { + return; + } + + this.out.append("\n"); + this.out.append(this.indent.repeat(this.stack.size())); + } + + /** + * Encodes the key (property name) to this stringer. + * @param name the name of the forthcoming value. May not be null. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer key(String name) throws JSONException { + if (name == null) { + throw new JSONException("Names must be non-null"); + } + beforeKey(); + string(name); + return this; + } + + /** + * Inserts any necessary separators and whitespace before a name. Also adjusts the + * stack to expect the key's value. + * @throws JSONException if processing of json failed + */ + private void beforeKey() throws JSONException { + Scope context = peek(); + if (context == Scope.NONEMPTY_OBJECT) { // first in object + this.out.append(','); + } + else if (context != Scope.EMPTY_OBJECT) { // not in an object! + throw new JSONException("Nesting problem"); + } + newline(); + replaceTop(Scope.DANGLING_KEY); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, inline + * array, or inline object. Also adjusts the stack to expect either a closing bracket + * or another element. + * @throws JSONException if processing of json failed + */ + private void beforeValue() throws JSONException { + if (this.stack.isEmpty()) { + return; + } + + Scope context = peek(); + if (context == Scope.EMPTY_ARRAY) { // first in array + replaceTop(Scope.NONEMPTY_ARRAY); + newline(); + } + else if (context == Scope.NONEMPTY_ARRAY) { // another in array + this.out.append(','); + newline(); + } + else if (context == Scope.DANGLING_KEY) { // value for key + this.out.append(this.indent == null ? ":" : ": "); + replaceTop(Scope.NONEMPTY_OBJECT); + } + else if (context != Scope.NULL) { + throw new JSONException("Nesting problem"); + } + } + + /** + * Returns the encoded JSON string. + *

+ * If invoked with unterminated arrays or unclosed objects, this method's return value + * is undefined. + *

+ * Warning: although it contradicts the general contract of + * {@link Object#toString}, this method returns null if the stringer contains no data. + * @return the encoded JSON string. + */ + @Override + public String toString() { + return this.out.isEmpty() ? null : this.out.toString(); + } + +} diff --git a/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONTokener.java b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONTokener.java new file mode 100644 index 00000000000..a8d3dfefc37 --- /dev/null +++ b/cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONTokener.java @@ -0,0 +1,555 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.cli.json; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * Parses a JSON (RFC 4627) encoded + * string into the corresponding object. Most clients of this class will use only need the + * {@link #JSONTokener(String) constructor} and {@link #nextValue} method. Example usage: + *

+ * String json = "{"
+ *         + "  \"query\": \"Pizza\", "
+ *         + "  \"locations\": [ 94043, 90210 ] "
+ *         + "}";
+ *
+ * JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
+ * String query = object.getString("query");
+ * JSONArray locations = object.getJSONArray("locations");
+ *

+ * For best interoperability and performance use JSON that complies with RFC 4627, such as + * that generated by {@link JSONStringer}. For legacy reasons this parser is lenient, so a + * successful parse does not indicate that the input string was valid JSON. All the + * following syntax errors will be ignored: + *

    + *
  • End of line comments starting with {@code //} or {@code #} and ending with a + * newline character. + *
  • C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such + * comments may not be nested. + *
  • Strings that are unquoted or {@code 'single quoted'}. + *
  • Hexadecimal integers prefixed with {@code 0x} or {@code 0X}. + *
  • Octal integers prefixed with {@code 0}. + *
  • Array elements separated by {@code ;}. + *
  • Unnecessary array separators. These are interpreted as if null was the omitted + * value. + *
  • Key-value pairs separated by {@code =} or {@code =>}. + *
  • Key-value pairs separated by {@code ;}. + *
+ *

+ * Each tokener may be used to parse a single JSON string. Instances of this class are not + * thread safe. Although this class is nonfinal, it was not designed for inheritance and + * should not be subclassed. In particular, self-use by overrideable methods is not + * specified. See Effective Java Item 17, "Design and Document or inheritance or + * else prohibit it" for further information. + */ +public class JSONTokener { + + /** + * The input JSON. + */ + private final String in; + + /** + * The index of the next character to be returned by {@link #next}. When the input is + * exhausted, this equals the input's length. + */ + private int pos; + + /** + * @param in JSON encoded string. Null is not permitted and will yield a tokener that + * throws {@code NullPointerExceptions} when methods are called. + */ + public JSONTokener(String in) { + // consume an optional byte order mark (BOM) if it exists + if (in != null && in.startsWith("\ufeff")) { + in = in.substring(1); + } + this.in = in; + } + + /** + * Returns the next value from the input. + * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, + * Double or {@link JSONObject#NULL}. + * @throws JSONException if the input is malformed. + */ + public Object nextValue() throws JSONException { + int c = nextCleanInternal(); + switch (c) { + case -1: + throw syntaxError("End of input"); + + case '{': + return readObject(); + + case '[': + return readArray(); + + case '\'', '"': + return nextString((char) c); + + default: + this.pos--; + return readLiteral(); + } + } + + private int nextCleanInternal() throws JSONException { + while (this.pos < this.in.length()) { + int c = this.in.charAt(this.pos++); + switch (c) { + case '\t', ' ', '\n', '\r': + continue; + + case '/': + if (this.pos == this.in.length()) { + return c; + } + + char peek = this.in.charAt(this.pos); + switch (peek) { + case '*': + // skip a /* c-style comment */ + this.pos++; + int commentEnd = this.in.indexOf("*/", this.pos); + if (commentEnd == -1) { + throw syntaxError("Unterminated comment"); + } + this.pos = commentEnd + 2; + continue; + + case '/': + // skip a // end-of-line comment + this.pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * Skip a # hash end-of-line comment. The JSON RFC doesn't specify + * this behavior, but it's required to parse existing documents. See + * https://b/2571423. + */ + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + return -1; + } + + /** + * Advances the position until after the next newline character. If the line is + * terminated by "\r\n", the '\n' must be consumed as whitespace by the caller. + */ + private void skipToEndOfLine() { + for (; this.pos < this.in.length(); this.pos++) { + char c = this.in.charAt(this.pos); + if (c == '\r' || c == '\n') { + this.pos++; + break; + } + } + } + + /** + * Returns the string up to but not including {@code quote}, unescaping any character + * escape sequences encountered along the way. The opening quote should have already + * been read. This consumes the closing quote, but does not include it in the returned + * string. + * @param quote either ' or ". + * @return the string up to but not including {@code quote} + * @throws NumberFormatException if any unicode escape sequences are malformed. + * @throws JSONException if processing of json failed + */ + public String nextString(char quote) throws JSONException { + /* + * For strings that are free of escape sequences, we can just extract the result + * as a substring of the input. But if we encounter an escape sequence, we need to + * use a StringBuilder to compose the result. + */ + StringBuilder builder = null; + + /* the index of the first character not yet appended to the builder. */ + int start = this.pos; + + while (this.pos < this.in.length()) { + int c = this.in.charAt(this.pos++); + if (c == quote) { + if (builder == null) { + // a new string avoids leaking memory + return new String(this.in.substring(start, this.pos - 1)); + } + else { + builder.append(this.in, start, this.pos - 1); + return builder.toString(); + } + } + + if (c == '\\') { + if (this.pos == this.in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(this.in, start, this.pos - 1); + builder.append(readEscapeCharacter()); + start = this.pos; + } + } + + throw syntaxError("Unterminated string"); + } + + /** + * Unescapes the character identified by the character or characters that immediately + * follow a backslash. The backslash '\' should have already been read. This supports + * both unicode escapes "u000A" and two-character escapes "\n". + * @return the unescaped char + * @throws NumberFormatException if any unicode escape sequences are malformed. + * @throws JSONException if processing of json failed + */ + private char readEscapeCharacter() throws JSONException { + char escaped = this.in.charAt(this.pos++); + switch (escaped) { + case 'u': + if (this.pos + 4 > this.in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = this.in.substring(this.pos, this.pos + 4); + this.pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'', '"', '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. Numeric values + * will be returned as an Integer, Long, or Double, in that order of preference. + * @return a literal value + * @throws JSONException if processing of json failed + */ + private Object readLiteral() throws JSONException { + String literal = nextToInternal("{}[]/\\:,=;# \t\f"); + + if (literal.isEmpty()) { + throw syntaxError("Expected literal value"); + } + else if ("null".equalsIgnoreCase(literal)) { + return JSONObject.NULL; + } + else if ("true".equalsIgnoreCase(literal)) { + return Boolean.TRUE; + } + else if ("false".equalsIgnoreCase(literal)) { + return Boolean.FALSE; + } + + /* try to parse as an integral type... */ + if (literal.indexOf('.') == -1) { + int base = 10; + String number = literal; + if (number.startsWith("0x") || number.startsWith("0X")) { + number = number.substring(2); + base = 16; + } + else if (number.startsWith("0") && number.length() > 1) { + number = number.substring(1); + base = 8; + } + try { + long longValue = Long.parseLong(number, base); + if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) { + return (int) longValue; + } + else { + return longValue; + } + } + catch (NumberFormatException e) { + /* + * This only happens for integral numbers greater than Long.MAX_VALUE, + * numbers in exponential form (5e-10) and unquoted strings. Fall through + * to try floating point. + */ + } + } + + /* ...next try to parse as a floating point... */ + try { + return Double.valueOf(literal); + } + catch (NumberFormatException ex) { + // Ignore + } + + /* ... finally give up. We have an unquoted string */ + return new String(literal); // a new string avoids leaking memory + } + + /** + * Returns the string up to but not including any of the given characters or a newline + * character. This does not consume the excluded character. + * @return the string up to but not including any of the given characters or a newline + * character + */ + private String nextToInternal(String excluded) { + int start = this.pos; + for (; this.pos < this.in.length(); this.pos++) { + char c = this.in.charAt(this.pos); + if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) { + return this.in.substring(start, this.pos); + } + } + return this.in.substring(start); + } + + /** + * Reads a sequence of key/value pairs and the trailing closing brace '}' of an + * object. The opening brace '{' should have already been read. + * @return an object + * @throws JSONException if processing of json failed + */ + private JSONObject readObject() throws JSONException { + JSONObject result = new JSONObject(); + + /* Peek to see if this is the empty object. */ + int first = nextCleanInternal(); + if (first == '}') { + return result; + } + else if (first != -1) { + this.pos--; + } + + while (true) { + Object name = nextValue(); + if (!(name instanceof String)) { + if (name == null) { + throw syntaxError("Names cannot be null"); + } + else { + throw syntaxError( + "Names must be strings, but " + name + " is of type " + name.getClass().getName()); + } + } + + /* + * Expect the name/value separator to be either a colon ':', an equals sign + * '=', or an arrow "=>". The last two are bogus but we include them because + * that's what the original implementation did. + */ + int separator = nextCleanInternal(); + if (separator != ':' && separator != '=') { + throw syntaxError("Expected ':' after " + name); + } + if (this.pos < this.in.length() && this.in.charAt(this.pos) == '>') { + this.pos++; + } + + result.put((String) name, nextValue()); + + switch (nextCleanInternal()) { + case '}': + return result; + case ';', ',': + continue; + default: + throw syntaxError("Unterminated object"); + } + } + } + + /** + * Reads a sequence of values and the trailing closing brace ']' of an array. The + * opening brace '[' should have already been read. Note that "[]" yields an empty + * array, but "[,]" returns a two-element array equivalent to "[null,null]". + * @return an array + * @throws JSONException if processing of json failed + */ + private JSONArray readArray() throws JSONException { + JSONArray result = new JSONArray(); + + /* to cover input that ends with ",]". */ + boolean hasTrailingSeparator = false; + + while (true) { + switch (nextCleanInternal()) { + case -1: + throw syntaxError("Unterminated array"); + case ']': + if (hasTrailingSeparator) { + result.put(null); + } + return result; + case ',', ';': + /* A separator without a value first means "null". */ + result.put(null); + hasTrailingSeparator = true; + continue; + default: + this.pos--; + } + + result.put(nextValue()); + + switch (nextCleanInternal()) { + case ']': + return result; + case ',', ';': + hasTrailingSeparator = true; + continue; + default: + throw syntaxError("Unterminated array"); + } + } + } + + /** + * Returns an exception containing the given message plus the current position and the + * entire input string. + * @param message the message + * @return an exception + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this); + } + + /** + * Returns the current position and the entire input string. + * @return the current position and the entire input string. + */ + @Override + public String toString() { + // consistent with the original implementation + return " at character " + this.pos + " of " + this.in; + } + + /* + * Legacy APIs. + * + * None of the methods below are on the critical path of parsing JSON documents. They + * exist only because they were exposed by the original implementation and may be used + * by some clients. + */ + + public boolean more() { + return this.pos < this.in.length(); + } + + public char next() { + return this.pos < this.in.length() ? this.in.charAt(this.pos++) : '\0'; + } + + public char next(char c) throws JSONException { + char result = next(); + if (result != c) { + throw syntaxError("Expected " + c + " but was " + result); + } + return result; + } + + public char nextClean() throws JSONException { + int nextCleanInt = nextCleanInternal(); + return nextCleanInt == -1 ? '\0' : (char) nextCleanInt; + } + + public String next(int length) throws JSONException { + if (this.pos + length > this.in.length()) { + throw syntaxError(length + " is out of bounds"); + } + String result = this.in.substring(this.pos, this.pos + length); + this.pos += length; + return result; + } + + public String nextTo(String excluded) { + if (excluded == null) { + throw new NullPointerException("excluded == null"); + } + return nextToInternal(excluded).trim(); + } + + public String nextTo(char excluded) { + return nextToInternal(String.valueOf(excluded)).trim(); + } + + public void skipPast(String thru) { + int thruStart = this.in.indexOf(thru, this.pos); + this.pos = thruStart == -1 ? this.in.length() : (thruStart + thru.length()); + } + + public char skipTo(char to) { + int index = this.in.indexOf(to, this.pos); + if (index != -1) { + this.pos = index; + return to; + } + else { + return '\0'; + } + } + + public void back() { + if (--this.pos == -1) { + this.pos = 0; + } + } + + public static int dehexchar(char hex) { + if (hex >= '0' && hex <= '9') { + return hex - '0'; + } + else if (hex >= 'A' && hex <= 'F') { + return hex - 'A' + 10; + } + else if (hex >= 'a' && hex <= 'f') { + return hex - 'a' + 10; + } + else { + return -1; + } + } + +} diff --git a/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java b/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java index dab9556e9fd..63688d0ed76 100644 --- a/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java +++ b/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java @@ -33,10 +33,10 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.message.StatusLine; -import org.json.JSONException; -import org.json.JSONObject; import org.jspecify.annotations.Nullable; +import org.springframework.boot.cli.json.JSONException; +import org.springframework.boot.cli.json.JSONObject; import org.springframework.boot.cli.util.Log; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; diff --git a/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java b/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java index 8e2bff69606..1577a94463d 100644 --- a/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java +++ b/cli/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java @@ -22,11 +22,11 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import org.jspecify.annotations.Nullable; +import org.springframework.boot.cli.json.JSONArray; +import org.springframework.boot.cli.json.JSONException; +import org.springframework.boot.cli.json.JSONObject; import org.springframework.util.Assert; /** diff --git a/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java b/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java index d9a687de5fd..a13975c3e9b 100644 --- a/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java +++ b/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java @@ -20,10 +20,10 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import org.json.JSONException; -import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.springframework.boot.cli.json.JSONException; +import org.springframework.boot.cli.json.JSONObject; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.util.StreamUtils; diff --git a/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java b/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java index 349c9de9f10..4489fa4d4c2 100644 --- a/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java +++ b/cli/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java @@ -23,10 +23,10 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; -import org.json.JSONException; -import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.springframework.boot.cli.json.JSONException; +import org.springframework.boot.cli.json.JSONObject; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.util.StreamUtils; diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 0b0e17a6c67..74d14e278a8 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -36,6 +36,8 @@ + + diff --git a/configuration-metadata/spring-boot-configuration-metadata/build.gradle b/configuration-metadata/spring-boot-configuration-metadata/build.gradle index ac3aba5f755..ecc899004f0 100644 --- a/configuration-metadata/spring-boot-configuration-metadata/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata/build.gradle @@ -21,9 +21,15 @@ plugins { description = "Spring Boot Configuration Metadata" -dependencies { - implementation("com.vaadin.external.google:android-json") +sourceSets { + main { + java { + srcDir file("src/json-shade/java") + } + } +} +dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.assertj:assertj-core") testImplementation("org.springframework:spring-core") diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/README.adoc b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/README.adoc new file mode 100644 index 00000000000..65480069457 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/README.adoc @@ -0,0 +1,5 @@ +## Shaded JSON + +This source was originally taken from `com.vaadin.external.google:android-json` which +provides a clean room re-implementation of the `org.json` APIs and does not include the +"Do not use for evil" clause. diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSON.java b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSON.java new file mode 100644 index 00000000000..6e261670ea7 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSON.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.configurationmetadata.json; + +class JSON { + + static double checkDouble(double d) throws JSONException { + if (Double.isInfinite(d) || Double.isNaN(d)) { + throw new JSONException("Forbidden numeric value: " + d); + } + return d; + } + + static Boolean toBoolean(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String stringValue) { + if ("true".equalsIgnoreCase(stringValue)) { + return true; + } + if ("false".equalsIgnoreCase(stringValue)) { + return false; + } + } + return null; + } + + static Double toDouble(Object value) { + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + if (value instanceof String) { + try { + return Double.valueOf((String) value); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return null; + } + + static Integer toInteger(Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof String) { + try { + return (int) Double.parseDouble((String) value); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return null; + } + + static Long toLong(Object value) { + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + try { + return (long) Double.parseDouble((String) value); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return null; + } + + static String toString(Object value) { + if (value instanceof String) { + return (String) value; + } + if (value != null) { + return String.valueOf(value); + } + return null; + } + + public static JSONException typeMismatch(Object indexOrName, Object actual, String requiredType) + throws JSONException { + if (actual == null) { + throw new JSONException("Value at " + indexOrName + " is null."); + } + throw new JSONException("Value " + actual + " at " + indexOrName + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); + } + + public static JSONException typeMismatch(Object actual, String requiredType) throws JSONException { + if (actual == null) { + throw new JSONException("Value is null."); + } + throw new JSONException("Value " + actual + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONArray.java b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONArray.java new file mode 100644 index 00000000000..d93e8e0ad01 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONArray.java @@ -0,0 +1,669 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.configurationmetadata.json; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * A dense indexed sequence of values. Values may be any mix of {@link JSONObject + * JSONObjects}, other {@link JSONArray JSONArrays}, Strings, Booleans, Integers, Longs, + * Doubles, {@code null} or {@link JSONObject#NULL}. Values may not be + * {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not + * listed here. + *

+ * {@code JSONArray} has the same type coercion behavior and optional/mandatory accessors + * as {@link JSONObject}. See that class' documentation for details. + *

+ * Warning: this class represents null in two incompatible ways: the + * standard Java {@code null} reference, and the sentinel value {@link JSONObject#NULL}. + * In particular, {@code get} fails if the requested index holds the null reference, but + * succeeds if it holds {@code JSONObject.NULL}. + *

+ * Instances of this class are not thread safe. Although this class is nonfinal, it was + * not designed for inheritance and should not be subclassed. In particular, self-use by + * overridable methods is not specified. See Effective Java Item 17, "Design and + * Document or inheritance or else prohibit it" for further information. + */ +public class JSONArray { + + private final List values; + + /** + * Creates a {@code JSONArray} with no values. + */ + public JSONArray() { + this.values = new ArrayList<>(); + } + + /** + * Creates a new {@code JSONArray} by copying all values from the given collection. + * @param copyFrom a collection whose values are of supported types. Unsupported + * values are not permitted and will yield an array in an inconsistent state. + */ + /* Accept a raw type for API compatibility */ + @SuppressWarnings("rawtypes") + public JSONArray(Collection copyFrom) { + this(); + if (copyFrom != null) { + for (Iterator it = copyFrom.iterator(); it.hasNext();) { + put(JSONObject.wrap(it.next())); + } + } + } + + /** + * Creates a new {@code JSONArray} with values from the next array in the tokener. + * @param readFrom a tokener whose nextValue() method will yield a {@code JSONArray}. + * @throws JSONException if the parse fails or doesn't yield a {@code JSONArray}. + * @throws JSONException if processing of json failed + */ + public JSONArray(JSONTokener readFrom) throws JSONException { + /* + * Getting the parser to populate this could get tricky. Instead, just parse to + * temporary JSONArray and then steal the data from that. + */ + Object object = readFrom.nextValue(); + if (object instanceof JSONArray) { + this.values = ((JSONArray) object).values; + } + else { + throw JSON.typeMismatch(object, "JSONArray"); + } + } + + /** + * Creates a new {@code JSONArray} with values from the JSON string. + * @param json a JSON-encoded string containing an array. + * @throws JSONException if the parse fails or doesn't yield a {@code + * JSONArray}. + */ + public JSONArray(String json) throws JSONException { + this(new JSONTokener(json)); + } + + /** + * Creates a new {@code JSONArray} with values from the given primitive array. + * @param array a primitive array + * @throws JSONException if processing of json failed + */ + public JSONArray(Object array) throws JSONException { + if (!array.getClass().isArray()) { + throw new JSONException("Not a primitive array: " + array.getClass()); + } + final int length = Array.getLength(array); + this.values = new ArrayList<>(length); + for (int i = 0; i < length; ++i) { + put(JSONObject.wrap(Array.get(array, i))); + } + } + + /** + * Returns the number of values in this array. + * @return the length of this array + */ + public int length() { + return this.values.size(); + } + + /** + * Appends {@code value} to the end of this array. + * @param value the value + * @return this array. + */ + public JSONArray put(boolean value) { + this.values.add(value); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(double value) throws JSONException { + this.values.add(JSON.checkDouble(value)); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value the value + * @return this array. + */ + public JSONArray put(int value) { + this.values.add(value); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value the value + * @return this array. + */ + public JSONArray put(long value) { + this.values.add(value); + return this; + } + + /** + * Appends {@code value} to the end of this array. + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be + * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. Unsupported + * values are not permitted and will cause the array to be in an inconsistent state. + * @return this array. + */ + public JSONArray put(Object value) { + this.values.add(value); + return this; + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value the value + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, boolean value) throws JSONException { + return put(index, (Boolean) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, double value) throws JSONException { + return put(index, (Double) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value the value + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, int value) throws JSONException { + return put(index, (Integer) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value the value + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, long value) throws JSONException { + return put(index, (Long) value); + } + + /** + * Sets the value at {@code index} to {@code value}, null padding this array to the + * required length if necessary. If a value already exists at {@code + * index}, it will be replaced. + * @param index the index to set the value to + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be + * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. + * @return this array. + * @throws JSONException if processing of json failed + */ + public JSONArray put(int index, Object value) throws JSONException { + if (value instanceof Number) { + // deviate from the original by checking all Numbers, not just floats & + // doubles + JSON.checkDouble(((Number) value).doubleValue()); + } + while (this.values.size() <= index) { + this.values.add(null); + } + this.values.set(index, value); + return this; + } + + /** + * Returns true if this array has no value at {@code index}, or if its value is the + * {@code null} reference or {@link JSONObject#NULL}. + * @param index the index to set the value to + * @return true if this array has no value at {@code index} + */ + public boolean isNull(int index) { + Object value = opt(index); + return value == null || value == JSONObject.NULL; + } + + /** + * Returns the value at {@code index}. + * @param index the index to get the value from + * @return the value at {@code index}. + * @throws JSONException if this array has no value at {@code index}, or if that value + * is the {@code null} reference. This method returns normally if the value is + * {@code JSONObject#NULL}. + */ + public Object get(int index) throws JSONException { + try { + Object value = this.values.get(index); + if (value == null) { + throw new JSONException("Value at " + index + " is null."); + } + return value; + } + catch (IndexOutOfBoundsException e) { + throw new JSONException("Index " + index + " out of range [0.." + this.values.size() + ")"); + } + } + + /** + * Returns the value at {@code index}, or null if the array has no value at + * {@code index}. + * @param index the index to get the value from + * @return the value at {@code index} or {@code null} + */ + public Object opt(int index) { + if (index < 0 || index >= this.values.size()) { + return null; + } + return this.values.get(index); + } + + /** + * Removes and returns the value at {@code index}, or null if the array has no value + * at {@code index}. + * @param index the index of the value to remove + * @return the previous value at {@code index} + */ + public Object remove(int index) { + if (index < 0 || index >= this.values.size()) { + return null; + } + return this.values.remove(index); + } + + /** + * Returns the value at {@code index} if it exists and is a boolean or can be coerced + * to a boolean. + * @param index the index to get the value from + * @return the value at {@code index} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to a boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = get(index); + Boolean result = JSON.toBoolean(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "boolean"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is a boolean or can be coerced + * to a boolean. Returns false otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code false} + */ + public boolean optBoolean(int index) { + return optBoolean(index, false); + } + + /** + * Returns the value at {@code index} if it exists and is a boolean or can be coerced + * to a boolean. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public boolean optBoolean(int index, boolean fallback) { + Object object = opt(index); + Boolean result = JSON.toBoolean(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is a double or can be coerced + * to a double. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to a double. + */ + public double getDouble(int index) throws JSONException { + Object object = get(index); + Double result = JSON.toDouble(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "double"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is a double or can be coerced + * to a double. Returns {@code NaN} otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code NaN} + */ + public double optDouble(int index) { + return optDouble(index, Double.NaN); + } + + /** + * Returns the value at {@code index} if it exists and is a double or can be coerced + * to a double. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public double optDouble(int index, double fallback) { + Object object = opt(index); + Double result = JSON.toDouble(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is an int or can be coerced to + * an int. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to an int. + */ + public int getInt(int index) throws JSONException { + Object object = get(index); + Integer result = JSON.toInteger(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "int"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is an int or can be coerced to + * an int. Returns 0 otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code 0} + */ + public int optInt(int index) { + return optInt(index, 0); + } + + /** + * Returns the value at {@code index} if it exists and is an int or can be coerced to + * an int. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public int optInt(int index, int fallback) { + Object object = opt(index); + Integer result = JSON.toInteger(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is a long or can be coerced to + * a long. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if the value at {@code index} doesn't exist or cannot be + * coerced to a long. + */ + public long getLong(int index) throws JSONException { + Object object = get(index); + Long result = JSON.toLong(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "long"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists and is a long or can be coerced to + * a long. Returns 0 otherwise. + * @param index the index to get the value from + * @return the {@code value} or {@code 0} + */ + public long optLong(int index) { + return optLong(index, 0L); + } + + /** + * Returns the value at {@code index} if it exists and is a long or can be coerced to + * a long. Returns {@code fallback} otherwise. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public long optLong(int index, long fallback) { + Object object = opt(index); + Long result = JSON.toLong(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists, coercing it if necessary. + * @param index the index to get the value from + * @return the {@code value} + * @throws JSONException if no such value exists. + */ + public String getString(int index) throws JSONException { + Object object = get(index); + String result = JSON.toString(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "String"); + } + return result; + } + + /** + * Returns the value at {@code index} if it exists, coercing it if necessary. Returns + * the empty string if no such value exists. + * @param index the index to get the value from + * @return the {@code value} or an empty string + */ + public String optString(int index) { + return optString(index, ""); + } + + /** + * Returns the value at {@code index} if it exists, coercing it if necessary. Returns + * {@code fallback} if no such value exists. + * @param index the index to get the value from + * @param fallback the fallback value + * @return the value at {@code index} of {@code fallback} + */ + public String optString(int index, String fallback) { + Object object = opt(index); + String result = JSON.toString(object); + return result != null ? result : fallback; + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONArray}. + * @param index the index to get the value from + * @return the array at {@code index} + * @throws JSONException if the value doesn't exist or is not a {@code + * JSONArray}. + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + else { + throw JSON.typeMismatch(index, object, "JSONArray"); + } + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONArray}. Returns null otherwise. + * @param index the index to get the value from + * @return the array at {@code index} or {@code null} + */ + public JSONArray optJSONArray(int index) { + Object object = opt(index); + return object instanceof JSONArray ? (JSONArray) object : null; + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONObject}. + * @param index the index to get the value from + * @return the object at {@code index} + * @throws JSONException if the value doesn't exist or is not a {@code + * JSONObject}. + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + else { + throw JSON.typeMismatch(index, object, "JSONObject"); + } + } + + /** + * Returns the value at {@code index} if it exists and is a {@code + * JSONObject}. Returns null otherwise. + * @param index the index to get the value from + * @return the object at {@code index} or {@code null} + */ + public JSONObject optJSONObject(int index) { + Object object = opt(index); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Returns a new object whose values are the values in this array, and whose names are + * the values in {@code names}. Names and values are paired up by index from 0 through + * to the shorter array's length. Names that are not strings will be coerced to + * strings. This method returns null if either array is empty. + * @param names the property names + * @return a json object + * @throws JSONException if processing of json failed + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + JSONObject result = new JSONObject(); + int length = Math.min(names.length(), this.values.size()); + if (length == 0) { + return null; + } + for (int i = 0; i < length; i++) { + String name = JSON.toString(names.opt(i)); + result.put(name, opt(i)); + } + return result; + } + + /** + * Returns a new string by alternating this array's values with {@code + * separator}. This array's string values are quoted and have their special characters + * escaped. For example, the array containing the strings '12" pizza', 'taco' and + * 'soda' joined on '+' returns this:
"12\" pizza"+"taco"+"soda"
+ * @param separator the separator to use + * @return the joined value + * @throws JSONException if processing of json failed + */ + public String join(String separator) throws JSONException { + JSONStringer stringer = new JSONStringer(); + stringer.open(JSONStringer.Scope.NULL, ""); + for (int i = 0, size = this.values.size(); i < size; i++) { + if (i > 0) { + stringer.out.append(separator); + } + stringer.value(this.values.get(i)); + } + stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); + return stringer.out.toString(); + } + + /** + * Encodes this array as a compact JSON string, such as:
[94043,90210]
+ * @return a compact JSON string representation of this array + */ + @Override + public String toString() { + try { + JSONStringer stringer = new JSONStringer(); + writeTo(stringer); + return stringer.toString(); + } + catch (JSONException e) { + return null; + } + } + + /** + * Encodes this array as a human-readable JSON string for debugging, such as:
+	 * [
+	 *     94043,
+	 *     90210
+	 * ]
+ * @param indentSpaces the number of spaces to indent for each level of nesting. + * @return a human-readable JSON string of this array + * @throws JSONException if processing of json failed + */ + public String toString(int indentSpaces) throws JSONException { + JSONStringer stringer = new JSONStringer(indentSpaces); + writeTo(stringer); + return stringer.toString(); + } + + void writeTo(JSONStringer stringer) throws JSONException { + stringer.array(); + for (Object value : this.values) { + stringer.value(value); + } + stringer.endArray(); + } + + @Override + public boolean equals(Object o) { + return o instanceof JSONArray && ((JSONArray) o).values.equals(this.values); + } + + @Override + public int hashCode() { + // diverge from the original, which doesn't implement hashCode + return this.values.hashCode(); + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONException.java b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONException.java new file mode 100644 index 00000000000..3b557b2b135 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.configurationmetadata.json; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * Thrown to indicate a problem with the JSON API. Such problems include: + *
    + *
  • Attempts to parse or construct malformed documents + *
  • Use of null as a name + *
  • Use of numeric types not available to JSON, such as {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + *
  • Lookups using an out of range index or nonexistent name + *
  • Type mismatches on lookups + *
+ *

+ * Although this is a checked exception, it is rarely recoverable. Most callers should + * simply wrap this exception in an unchecked exception and rethrow:

+ *     public JSONArray toJSONObject() {
+ *     try {
+ *         JSONObject result = new JSONObject();
+ *         ...
+ *     } catch (JSONException e) {
+ *         throw new RuntimeException(e);
+ *     }
+ * }
+ */ +public class JSONException extends Exception { + + public JSONException(String s) { + super(s); + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONObject.java b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONObject.java new file mode 100644 index 00000000000..f03bbe6cb8e --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONObject.java @@ -0,0 +1,836 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.configurationmetadata.json; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * A modifiable set of name/value mappings. Names are unique, non-null strings. Values may + * be any mix of {@link JSONObject JSONObjects}, {@link JSONArray JSONArrays}, Strings, + * Booleans, Integers, Longs, Doubles or {@link #NULL}. Values may not be {@code null}, + * {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities}, or of any type not + * listed here. + *

+ * This class can coerce values to another type when requested. + *

+ *

+ * This class can look up both mandatory and optional values: + *

    + *
  • Use getType() to retrieve a mandatory value. This fails with a + * {@code JSONException} if the requested name has no value or if the value cannot be + * coerced to the requested type. + *
  • Use optType() to retrieve an optional value. This returns a + * system- or user-supplied default if the requested name has no value or if the value + * cannot be coerced to the requested type. + *
+ *

+ * Warning: this class represents null in two incompatible ways: the + * standard Java {@code null} reference, and the sentinel value {@link JSONObject#NULL}. + * In particular, calling {@code put(name, null)} removes the named entry from the object + * but {@code put(name, JSONObject.NULL)} stores an entry whose value is + * {@code JSONObject.NULL}. + *

+ * Instances of this class are not thread safe. Although this class is nonfinal, it was + * not designed for inheritance and should not be subclassed. In particular, self-use by + * overrideable methods is not specified. See Effective Java Item 17, "Design and + * Document or inheritance or else prohibit it" for further information. + */ +public class JSONObject { + + private static final Double NEGATIVE_ZERO = -0d; + + /** + * A sentinel value used to explicitly define a name with no value. Unlike + * {@code null}, names with this value: + *

    + *
  • show up in the {@link #names} array + *
  • show up in the {@link #keys} iterator + *
  • return {@code true} for {@link #has(String)} + *
  • do not throw on {@link #get(String)} + *
  • are included in the encoded JSON string. + *
+ *

+ * This value violates the general contract of {@link Object#equals} by returning true + * when compared to {@code null}. Its {@link #toString} method returns "null". + */ + public static final Object NULL = new Object() { + + @Override + public boolean equals(Object o) { + return o == this || o == null; // API specifies this broken equals + // implementation + } + + @Override + public String toString() { + return "null"; + } + + }; + + private final Map nameValuePairs; + + /** + * Creates a {@code JSONObject} with no name/value mappings. + */ + public JSONObject() { + this.nameValuePairs = new LinkedHashMap<>(); + } + + /** + * Creates a new {@code JSONObject} by copying all name/value mappings from the given + * map. + * @param copyFrom a map whose keys are of type {@link String} and whose values are of + * supported types. + * @throws NullPointerException if any of the map's keys are null. + */ + /* (accept a raw type for API compatibility) */ + @SuppressWarnings("rawtypes") + public JSONObject(Map copyFrom) { + this(); + Map contentsTyped = copyFrom; + for (Map.Entry entry : contentsTyped.entrySet()) { + /* + * Deviate from the original by checking that keys are non-null and of the + * proper type. (We still defer validating the values). + */ + String key = (String) entry.getKey(); + if (key == null) { + throw new NullPointerException("key == null"); + } + this.nameValuePairs.put(key, wrap(entry.getValue())); + } + } + + /** + * Creates a new {@code JSONObject} with name/value mappings from the next object in + * the tokener. + * @param readFrom a tokener whose nextValue() method will yield a {@code JSONObject}. + * @throws JSONException if the parse fails or doesn't yield a {@code JSONObject}. + */ + public JSONObject(JSONTokener readFrom) throws JSONException { + /* + * Getting the parser to populate this could get tricky. Instead, just parse to + * temporary JSONObject and then steal the data from that. + */ + Object object = readFrom.nextValue(); + if (object instanceof JSONObject) { + this.nameValuePairs = ((JSONObject) object).nameValuePairs; + } + else { + throw JSON.typeMismatch(object, "JSONObject"); + } + } + + /** + * Creates a new {@code JSONObject} with name/value mappings from the JSON string. + * @param json a JSON-encoded string containing an object. + * @throws JSONException if the parse fails or doesn't yield a {@code + * JSONObject}. + */ + public JSONObject(String json) throws JSONException { + this(new JSONTokener(json)); + } + + /** + * Creates a new {@code JSONObject} by copying mappings for the listed names from the + * given object. Names that aren't present in {@code copyFrom} will be skipped. + * @param copyFrom the source + * @param names the property names + * @throws JSONException if an error occurs + */ + public JSONObject(JSONObject copyFrom, String[] names) throws JSONException { + this(); + for (String name : names) { + Object value = copyFrom.opt(name); + if (value != null) { + this.nameValuePairs.put(name, value); + } + } + } + + /** + * Returns the number of name/value mappings in this object. + * @return the number of name/value mappings in this object + */ + public int length() { + return this.nameValuePairs.size(); + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, boolean value) throws JSONException { + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, double value) throws JSONException { + this.nameValuePairs.put(checkName(name), JSON.checkDouble(value)); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, int value) throws JSONException { + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, long value) throws JSONException { + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Maps {@code name} to {@code value}, clobbering any existing name/value mapping with + * the same name. If the value is {@code null}, any existing mapping for {@code name} + * is removed. + * @param name the name of the property + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link #NULL}, or {@code null}. May not be {@link Double#isNaN() + * NaNs} or {@link Double#isInfinite() infinities}. + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject put(String name, Object value) throws JSONException { + if (value == null) { + this.nameValuePairs.remove(name); + return this; + } + if (value instanceof Number) { + // deviate from the original by checking all Numbers, not just floats & + // doubles + JSON.checkDouble(((Number) value).doubleValue()); + } + this.nameValuePairs.put(checkName(name), value); + return this; + } + + /** + * Equivalent to {@code put(name, value)} when both parameters are non-null; does + * nothing otherwise. + * @param name the name of the property + * @param value the value of the property + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject putOpt(String name, Object value) throws JSONException { + if (name == null || value == null) { + return this; + } + return put(name, value); + } + + /** + * Appends {@code value} to the array already mapped to {@code name}. If this object + * has no mapping for {@code name}, this inserts a new mapping. If the mapping exists + * but its value is not an array, the existing and new values are inserted in order + * into a new array which is itself mapped to {@code name}. In aggregate, this allows + * values to be added to a mapping one at a time. + * @param name the name of the property + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double, {@link #NULL} or null. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this object. + * @throws JSONException if an error occurs + */ + public JSONObject accumulate(String name, Object value) throws JSONException { + Object current = this.nameValuePairs.get(checkName(name)); + if (current == null) { + return put(name, value); + } + + // check in accumulate, since array.put(Object) doesn't do any checking + if (value instanceof Number) { + JSON.checkDouble(((Number) value).doubleValue()); + } + + if (current instanceof JSONArray array) { + array.put(value); + } + else { + JSONArray array = new JSONArray(); + array.put(current); + array.put(value); + this.nameValuePairs.put(name, array); + } + return this; + } + + String checkName(String name) throws JSONException { + if (name == null) { + throw new JSONException("Names must be non-null"); + } + return name; + } + + /** + * Removes the named mapping if it exists; does nothing otherwise. + * @param name the name of the property + * @return the value previously mapped by {@code name}, or null if there was no such + * mapping. + */ + public Object remove(String name) { + return this.nameValuePairs.remove(name); + } + + /** + * Returns true if this object has no mapping for {@code name} or if it has a mapping + * whose value is {@link #NULL}. + * @param name the name of the property + * @return true if this object has no mapping for {@code name} + */ + public boolean isNull(String name) { + Object value = this.nameValuePairs.get(name); + return value == null || value == NULL; + } + + /** + * Returns true if this object has a mapping for {@code name}. The mapping may be + * {@link #NULL}. + * @param name the name of the property + * @return true if this object has a mapping for {@code name} + */ + public boolean has(String name) { + return this.nameValuePairs.containsKey(name); + } + + /** + * Returns the value mapped by {@code name}. + * @param name the name of the property + * @return the value + * @throws JSONException if no such mapping exists. + */ + public Object get(String name) throws JSONException { + Object result = this.nameValuePairs.get(name); + if (result == null) { + throw new JSONException("No value for " + name); + } + return result; + } + + /** + * Returns the value mapped by {@code name}, or null if no such mapping exists. + * @param name the name of the property + * @return the value or {@code null} + */ + public Object opt(String name) { + return this.nameValuePairs.get(name); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a boolean or can be + * coerced to a boolean. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to a + * boolean. + */ + public boolean getBoolean(String name) throws JSONException { + Object object = get(name); + Boolean result = JSON.toBoolean(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "boolean"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a boolean or can be + * coerced to a boolean. Returns false otherwise. + * @param name the name of the property + * @return the value or {@code null} + */ + public boolean optBoolean(String name) { + return optBoolean(name, false); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a boolean or can be + * coerced to a boolean. Returns {@code fallback} otherwise. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public boolean optBoolean(String name, boolean fallback) { + Object object = opt(name); + Boolean result = JSON.toBoolean(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a double or can be + * coerced to a double. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to a + * double. + */ + public double getDouble(String name) throws JSONException { + Object object = get(name); + Double result = JSON.toDouble(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "double"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a double or can be + * coerced to a double. Returns {@code NaN} otherwise. + * @param name the name of the property + * @return the value or {@code NaN} + */ + public double optDouble(String name) { + return optDouble(name, Double.NaN); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a double or can be + * coerced to a double. Returns {@code fallback} otherwise. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public double optDouble(String name, double fallback) { + Object object = opt(name); + Double result = JSON.toDouble(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is an int or can be + * coerced to an int. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to an int. + */ + public int getInt(String name) throws JSONException { + Object object = get(name); + Integer result = JSON.toInteger(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "int"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is an int or can be + * coerced to an int. Returns 0 otherwise. + * @param name the name of the property + * @return the value of {@code 0} + */ + public int optInt(String name) { + return optInt(name, 0); + } + + /** + * Returns the value mapped by {@code name} if it exists and is an int or can be + * coerced to an int. Returns {@code fallback} otherwise. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public int optInt(String name, int fallback) { + Object object = opt(name); + Integer result = JSON.toInteger(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a long or can be + * coerced to a long. Note that JSON represents numbers as doubles, so this is + * lossy; use strings to transfer numbers over JSON. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or cannot be coerced to a long. + */ + public long getLong(String name) throws JSONException { + Object object = get(name); + Long result = JSON.toLong(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "long"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a long or can be + * coerced to a long. Returns 0 otherwise. Note that JSON represents numbers as + * doubles, so this is lossy; use strings to transfer numbers via + * JSON. + * @param name the name of the property + * @return the value or {@code 0L} + */ + public long optLong(String name) { + return optLong(name, 0L); + } + + /** + * Returns the value mapped by {@code name} if it exists and is a long or can be + * coerced to a long. Returns {@code fallback} otherwise. Note that JSON represents + * numbers as doubles, so this is lossy; use strings to transfer + * numbers over JSON. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public long optLong(String name, long fallback) { + Object object = opt(name); + Long result = JSON.toLong(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists, coercing it if necessary. + * @param name the name of the property + * @return the value + * @throws JSONException if no such mapping exists. + */ + public String getString(String name) throws JSONException { + Object object = get(name); + String result = JSON.toString(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "String"); + } + return result; + } + + /** + * Returns the value mapped by {@code name} if it exists, coercing it if necessary. + * Returns the empty string if no such mapping exists. + * @param name the name of the property + * @return the value or an empty string + */ + public String optString(String name) { + return optString(name, ""); + } + + /** + * Returns the value mapped by {@code name} if it exists, coercing it if necessary. + * Returns {@code fallback} if no such mapping exists. + * @param name the name of the property + * @param fallback a fallback value + * @return the value or {@code fallback} + */ + public String optString(String name, String fallback) { + Object object = opt(name); + String result = JSON.toString(object); + return result != null ? result : fallback; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONArray}. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or is not a {@code + * JSONArray}. + */ + public JSONArray getJSONArray(String name) throws JSONException { + Object object = get(name); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + else { + throw JSON.typeMismatch(name, object, "JSONArray"); + } + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONArray}. Returns null otherwise. + * @param name the name of the property + * @return the value or {@code null} + */ + public JSONArray optJSONArray(String name) { + Object object = opt(name); + return object instanceof JSONArray ? (JSONArray) object : null; + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONObject}. + * @param name the name of the property + * @return the value + * @throws JSONException if the mapping doesn't exist or is not a {@code + * JSONObject}. + */ + public JSONObject getJSONObject(String name) throws JSONException { + Object object = get(name); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + else { + throw JSON.typeMismatch(name, object, "JSONObject"); + } + } + + /** + * Returns the value mapped by {@code name} if it exists and is a {@code + * JSONObject}. Returns null otherwise. + * @param name the name of the property + * @return the value or {@code null} + */ + public JSONObject optJSONObject(String name) { + Object object = opt(name); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Returns an array with the values corresponding to {@code names}. The array contains + * null for names that aren't mapped. This method returns null if {@code names} is + * either null or empty. + * @param names the names of the properties + * @return the array + */ + public JSONArray toJSONArray(JSONArray names) { + JSONArray result = new JSONArray(); + if (names == null) { + return null; + } + int length = names.length(); + if (length == 0) { + return null; + } + for (int i = 0; i < length; i++) { + String name = JSON.toString(names.opt(i)); + result.put(opt(name)); + } + return result; + } + + /** + * Returns an iterator of the {@code String} names in this object. The returned + * iterator supports {@link Iterator#remove() remove}, which will remove the + * corresponding mapping from this object. If this object is modified after the + * iterator is returned, the iterator's behavior is undefined. The order of the keys + * is undefined. + * @return the keys + */ + /* Return a raw type for API compatibility */ + @SuppressWarnings("rawtypes") + public Iterator keys() { + return this.nameValuePairs.keySet().iterator(); + } + + /** + * Returns an array containing the string names in this object. This method returns + * null if this object contains no mappings. + * @return the array + */ + public JSONArray names() { + return this.nameValuePairs.isEmpty() ? null : new JSONArray(new ArrayList<>(this.nameValuePairs.keySet())); + } + + /** + * Encodes this object as a compact JSON string, such as: + *

{"query":"Pizza","locations":[94043,90210]}
+ * @return a string representation of the object. + */ + @Override + public String toString() { + try { + JSONStringer stringer = new JSONStringer(); + writeTo(stringer); + return stringer.toString(); + } + catch (JSONException e) { + return null; + } + } + + /** + * Encodes this object as a human-readable JSON string for debugging, such as:
+	 * {
+	 *     "query": "Pizza",
+	 *     "locations": [
+	 *         94043,
+	 *         90210
+	 *     ]
+	 * }
+ * @param indentSpaces the number of spaces to indent for each level of nesting. + * @return a string representation of the object. + * @throws JSONException if an error occurs + */ + public String toString(int indentSpaces) throws JSONException { + JSONStringer stringer = new JSONStringer(indentSpaces); + writeTo(stringer); + return stringer.toString(); + } + + void writeTo(JSONStringer stringer) throws JSONException { + stringer.object(); + for (Map.Entry entry : this.nameValuePairs.entrySet()) { + stringer.key(entry.getKey()).value(entry.getValue()); + } + stringer.endObject(); + } + + /** + * Encodes the number as a JSON string. + * @param number a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return the encoded value + * @throws JSONException if an error occurs + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Number must be non-null"); + } + + double doubleValue = number.doubleValue(); + JSON.checkDouble(doubleValue); + + // the original returns "-0" instead of "-0.0" for negative zero + if (number.equals(NEGATIVE_ZERO)) { + return "-0"; + } + + long longValue = number.longValue(); + if (doubleValue == longValue) { + return Long.toString(longValue); + } + + return number.toString(); + } + + /** + * Encodes {@code data} as a JSON string. This applies quotes and any necessary + * character escaping. + * @param data the string to encode. Null will be interpreted as an empty string. + * @return the quoted value + */ + public static String quote(String data) { + if (data == null) { + return "\"\""; + } + try { + JSONStringer stringer = new JSONStringer(); + stringer.open(JSONStringer.Scope.NULL, ""); + stringer.value(data); + stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); + return stringer.toString(); + } + catch (JSONException e) { + throw new AssertionError(); + } + } + + /** + * Wraps the given object if necessary. + *

+ * If the object is null or, returns {@link #NULL}. If the object is a + * {@code JSONArray} or {@code JSONObject}, no wrapping is necessary. If the object is + * {@code NULL}, no wrapping is necessary. If the object is an array or + * {@code Collection}, returns an equivalent {@code JSONArray}. If the object is a + * {@code Map}, returns an equivalent {@code JSONObject}. If the object is a primitive + * wrapper type or {@code String}, returns the object. Otherwise if the object is from + * a {@code java} package, returns the result of {@code toString}. If wrapping fails, + * returns null. + * @param o the object to wrap + * @return the wrapped object + */ + @SuppressWarnings("rawtypes") + public static Object wrap(Object o) { + if (o == null) { + return NULL; + } + if (o instanceof JSONArray || o instanceof JSONObject) { + return o; + } + if (o.equals(NULL)) { + return o; + } + try { + if (o instanceof Collection) { + return new JSONArray((Collection) o); + } + else if (o.getClass().isArray()) { + return new JSONArray(o); + } + if (o instanceof Map) { + return new JSONObject((Map) o); + } + if (o instanceof Boolean || o instanceof Byte || o instanceof Character || o instanceof Double + || o instanceof Float || o instanceof Integer || o instanceof Long || o instanceof Short + || o instanceof String) { + return o; + } + if (o.getClass().getPackage().getName().startsWith("java.")) { + return o.toString(); + } + } + catch (Exception ex) { + // Ignore + } + return null; + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONStringer.java b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONStringer.java new file mode 100644 index 00000000000..0f72049a133 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONStringer.java @@ -0,0 +1,429 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.configurationmetadata.json; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most application + * developers should use those methods directly and disregard this API. For example:

+ * JSONObject object = ...
+ * String json = object.toString();
+ *

+ * Stringers only encode well-formed JSON strings. In particular: + *

    + *
  • The stringer must have exactly one top-level array or object. + *
  • Lexical scopes must be balanced: every call to {@link #array} must have a matching + * call to {@link #endArray} and every call to {@link #object} must have a matching call + * to {@link #endObject}. + *
  • Arrays may not contain keys (property names). + *
  • Objects must alternate keys (property names) and values. + *
  • Values are inserted with either literal {@link #value(Object) value} calls, or by + * nesting arrays or objects. + *
+ * Calls that would result in a malformed JSON string will fail with a + * {@link JSONException}. + *

+ * This class provides no facility for pretty-printing (ie. indenting) output. To encode + * indented output, use {@link JSONObject#toString(int)} or + * {@link JSONArray#toString(int)}. + *

+ * Some implementations of the API support at most 20 levels of nesting. Attempts to + * create more than 20 levels of nesting may fail with a {@link JSONException}. + *

+ * Each stringer may be used to encode a single top level value. Instances of this class + * are not thread safe. Although this class is nonfinal, it was not designed for + * inheritance and should not be subclassed. In particular, self-use by overrideable + * methods is not specified. See Effective Java Item 17, "Design and Document or + * inheritance or else prohibit it" for further information. + */ +public class JSONStringer { + + /** + * The output data, containing at most one top-level array or object. + */ + final StringBuilder out = new StringBuilder(); + + /** + * Lexical scoping elements within this stringer, necessary to insert the appropriate + * separator characters (i.e. commas and colons) and to detect nesting errors. + */ + enum Scope { + + /** + * An array with no elements requires no separators or newlines before it is + * closed. + */ + EMPTY_ARRAY, + + /** + * An array with at least one value requires a comma and newline before the next + * element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no keys or values requires no separators or newlines before it + * is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must be a value. + */ + DANGLING_KEY, + + /** + * An object with at least one name/value pair requires a comma and newline before + * the next element. + */ + NONEMPTY_OBJECT, + + /** + * A special bracketless array needed by JSONStringer.join() and + * JSONObject.quote() only. Not used for JSON encoding. + */ + NULL + + } + + /** + * Unlike the original implementation, this stack isn't limited to 20 levels of + * nesting. + */ + private final List stack = new ArrayList<>(); + + /** + * A string containing a full set of spaces for a single level of indentation, or null + * for no pretty printing. + */ + private final String indent; + + public JSONStringer() { + this.indent = null; + } + + JSONStringer(int indentSpaces) { + char[] indentChars = new char[indentSpaces]; + Arrays.fill(indentChars, ' '); + this.indent = new String(indentChars); + } + + /** + * Begins encoding a new array. Each call to this method must be paired with a call to + * {@link #endArray}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer array() throws JSONException { + return open(Scope.EMPTY_ARRAY, "["); + } + + /** + * Ends encoding the current array. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer endArray() throws JSONException { + return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]"); + } + + /** + * Begins encoding a new object. Each call to this method must be paired with a call + * to {@link #endObject}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer object() throws JSONException { + return open(Scope.EMPTY_OBJECT, "{"); + } + + /** + * Ends encoding the current object. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer endObject() throws JSONException { + return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given bracket. + * @param empty any necessary whitespace + * @param openBracket the open bracket + * @return this object + * @throws JSONException if processing of json failed + */ + JSONStringer open(Scope empty, String openBracket) throws JSONException { + if (this.stack.isEmpty() && !this.out.isEmpty()) { + throw new JSONException("Nesting problem: multiple top-level roots"); + } + beforeValue(); + this.stack.add(empty); + this.out.append(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the given + * bracket. + * @param empty any necessary whitespace + * @param nonempty the current scope + * @param closeBracket the close bracket + * @return the JSON stringer + * @throws JSONException if processing of json failed + */ + JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException { + Scope context = peek(); + if (context != nonempty && context != empty) { + throw new JSONException("Nesting problem"); + } + + this.stack.remove(this.stack.size() - 1); + if (context == nonempty) { + newline(); + } + this.out.append(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + * @return the scope + * @throws JSONException if processing of json failed + */ + private Scope peek() throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + return this.stack.get(this.stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + * @param topOfStack the scope at the top of the stack + */ + private void replaceTop(Scope topOfStack) { + this.stack.set(this.stack.size() - 1, topOfStack); + } + + /** + * Encodes {@code value}. + * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, + * Long, Double or null. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(Object value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + + if (value instanceof JSONArray) { + ((JSONArray) value).writeTo(this); + return this; + } + else if (value instanceof JSONObject) { + ((JSONObject) value).writeTo(this); + return this; + } + + beforeValue(); + + if (value == null || value instanceof Boolean || value == JSONObject.NULL) { + this.out.append(value); + + } + else if (value instanceof Number) { + this.out.append(JSONObject.numberToString((Number) value)); + + } + else { + string(value.toString()); + } + + return this; + } + + /** + * Encodes {@code value} to this stringer. + * @param value the value to encode + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(boolean value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + this.out.append(value); + return this; + } + + /** + * Encodes {@code value} to this stringer. + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(double value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + this.out.append(JSONObject.numberToString(value)); + return this; + } + + /** + * Encodes {@code value} to this stringer. + * @param value the value to encode + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer value(long value) throws JSONException { + if (this.stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + this.out.append(value); + return this; + } + + private void string(String value) { + this.out.append("\""); + for (int i = 0, length = value.length(); i < length; i++) { + char c = value.charAt(i); + + /* + * From RFC 4627, "All Unicode characters may be placed within the quotation + * marks except for the characters that must be escaped: quotation mark, + * reverse solidus, and the control characters (U+0000 through U+001F)." + */ + switch (c) { + case '"', '\\', '/' -> this.out.append('\\').append(c); + case '\t' -> this.out.append("\\t"); + case '\b' -> this.out.append("\\b"); + case '\n' -> this.out.append("\\n"); + case '\r' -> this.out.append("\\r"); + case '\f' -> this.out.append("\\f"); + default -> { + if (c <= 0x1F) { + this.out.append(String.format("\\u%04x", (int) c)); + } + else { + this.out.append(c); + } + } + } + + } + this.out.append("\""); + } + + private void newline() { + if (this.indent == null) { + return; + } + + this.out.append("\n"); + this.out.append(this.indent.repeat(this.stack.size())); + } + + /** + * Encodes the key (property name) to this stringer. + * @param name the name of the forthcoming value. May not be null. + * @return this stringer. + * @throws JSONException if processing of json failed + */ + public JSONStringer key(String name) throws JSONException { + if (name == null) { + throw new JSONException("Names must be non-null"); + } + beforeKey(); + string(name); + return this; + } + + /** + * Inserts any necessary separators and whitespace before a name. Also adjusts the + * stack to expect the key's value. + * @throws JSONException if processing of json failed + */ + private void beforeKey() throws JSONException { + Scope context = peek(); + if (context == Scope.NONEMPTY_OBJECT) { // first in object + this.out.append(','); + } + else if (context != Scope.EMPTY_OBJECT) { // not in an object! + throw new JSONException("Nesting problem"); + } + newline(); + replaceTop(Scope.DANGLING_KEY); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, inline + * array, or inline object. Also adjusts the stack to expect either a closing bracket + * or another element. + * @throws JSONException if processing of json failed + */ + private void beforeValue() throws JSONException { + if (this.stack.isEmpty()) { + return; + } + + Scope context = peek(); + if (context == Scope.EMPTY_ARRAY) { // first in array + replaceTop(Scope.NONEMPTY_ARRAY); + newline(); + } + else if (context == Scope.NONEMPTY_ARRAY) { // another in array + this.out.append(','); + newline(); + } + else if (context == Scope.DANGLING_KEY) { // value for key + this.out.append(this.indent == null ? ":" : ": "); + replaceTop(Scope.NONEMPTY_OBJECT); + } + else if (context != Scope.NULL) { + throw new JSONException("Nesting problem"); + } + } + + /** + * Returns the encoded JSON string. + *

+ * If invoked with unterminated arrays or unclosed objects, this method's return value + * is undefined. + *

+ * Warning: although it contradicts the general contract of + * {@link Object#toString}, this method returns null if the stringer contains no data. + * @return the encoded JSON string. + */ + @Override + public String toString() { + return this.out.isEmpty() ? null : this.out.toString(); + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONTokener.java b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONTokener.java new file mode 100644 index 00000000000..632a7df1be8 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata/src/json-shade/java/org/springframework/boot/configurationmetadata/json/JSONTokener.java @@ -0,0 +1,555 @@ +/* + * Copyright 2012-present the original author or 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 + * + * https://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 org.springframework.boot.configurationmetadata.json; + +// Note: this class was written without inspecting the non-free org.json source code. + +/** + * Parses a JSON (RFC 4627) encoded + * string into the corresponding object. Most clients of this class will use only need the + * {@link #JSONTokener(String) constructor} and {@link #nextValue} method. Example usage: + *

+ * String json = "{"
+ *         + "  \"query\": \"Pizza\", "
+ *         + "  \"locations\": [ 94043, 90210 ] "
+ *         + "}";
+ *
+ * JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
+ * String query = object.getString("query");
+ * JSONArray locations = object.getJSONArray("locations");
+ *

+ * For best interoperability and performance use JSON that complies with RFC 4627, such as + * that generated by {@link JSONStringer}. For legacy reasons this parser is lenient, so a + * successful parse does not indicate that the input string was valid JSON. All the + * following syntax errors will be ignored: + *

    + *
  • End of line comments starting with {@code //} or {@code #} and ending with a + * newline character. + *
  • C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such + * comments may not be nested. + *
  • Strings that are unquoted or {@code 'single quoted'}. + *
  • Hexadecimal integers prefixed with {@code 0x} or {@code 0X}. + *
  • Octal integers prefixed with {@code 0}. + *
  • Array elements separated by {@code ;}. + *
  • Unnecessary array separators. These are interpreted as if null was the omitted + * value. + *
  • Key-value pairs separated by {@code =} or {@code =>}. + *
  • Key-value pairs separated by {@code ;}. + *
+ *

+ * Each tokener may be used to parse a single JSON string. Instances of this class are not + * thread safe. Although this class is nonfinal, it was not designed for inheritance and + * should not be subclassed. In particular, self-use by overrideable methods is not + * specified. See Effective Java Item 17, "Design and Document or inheritance or + * else prohibit it" for further information. + */ +public class JSONTokener { + + /** + * The input JSON. + */ + private final String in; + + /** + * The index of the next character to be returned by {@link #next}. When the input is + * exhausted, this equals the input's length. + */ + private int pos; + + /** + * @param in JSON encoded string. Null is not permitted and will yield a tokener that + * throws {@code NullPointerExceptions} when methods are called. + */ + public JSONTokener(String in) { + // consume an optional byte order mark (BOM) if it exists + if (in != null && in.startsWith("\ufeff")) { + in = in.substring(1); + } + this.in = in; + } + + /** + * Returns the next value from the input. + * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, + * Double or {@link JSONObject#NULL}. + * @throws JSONException if the input is malformed. + */ + public Object nextValue() throws JSONException { + int c = nextCleanInternal(); + switch (c) { + case -1: + throw syntaxError("End of input"); + + case '{': + return readObject(); + + case '[': + return readArray(); + + case '\'', '"': + return nextString((char) c); + + default: + this.pos--; + return readLiteral(); + } + } + + private int nextCleanInternal() throws JSONException { + while (this.pos < this.in.length()) { + int c = this.in.charAt(this.pos++); + switch (c) { + case '\t', ' ', '\n', '\r': + continue; + + case '/': + if (this.pos == this.in.length()) { + return c; + } + + char peek = this.in.charAt(this.pos); + switch (peek) { + case '*': + // skip a /* c-style comment */ + this.pos++; + int commentEnd = this.in.indexOf("*/", this.pos); + if (commentEnd == -1) { + throw syntaxError("Unterminated comment"); + } + this.pos = commentEnd + 2; + continue; + + case '/': + // skip a // end-of-line comment + this.pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * Skip a # hash end-of-line comment. The JSON RFC doesn't specify + * this behavior, but it's required to parse existing documents. See + * https://b/2571423. + */ + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + return -1; + } + + /** + * Advances the position until after the next newline character. If the line is + * terminated by "\r\n", the '\n' must be consumed as whitespace by the caller. + */ + private void skipToEndOfLine() { + for (; this.pos < this.in.length(); this.pos++) { + char c = this.in.charAt(this.pos); + if (c == '\r' || c == '\n') { + this.pos++; + break; + } + } + } + + /** + * Returns the string up to but not including {@code quote}, unescaping any character + * escape sequences encountered along the way. The opening quote should have already + * been read. This consumes the closing quote, but does not include it in the returned + * string. + * @param quote either ' or ". + * @return the string up to but not including {@code quote} + * @throws NumberFormatException if any unicode escape sequences are malformed. + * @throws JSONException if processing of json failed + */ + public String nextString(char quote) throws JSONException { + /* + * For strings that are free of escape sequences, we can just extract the result + * as a substring of the input. But if we encounter an escape sequence, we need to + * use a StringBuilder to compose the result. + */ + StringBuilder builder = null; + + /* the index of the first character not yet appended to the builder. */ + int start = this.pos; + + while (this.pos < this.in.length()) { + int c = this.in.charAt(this.pos++); + if (c == quote) { + if (builder == null) { + // a new string avoids leaking memory + return new String(this.in.substring(start, this.pos - 1)); + } + else { + builder.append(this.in, start, this.pos - 1); + return builder.toString(); + } + } + + if (c == '\\') { + if (this.pos == this.in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(this.in, start, this.pos - 1); + builder.append(readEscapeCharacter()); + start = this.pos; + } + } + + throw syntaxError("Unterminated string"); + } + + /** + * Unescapes the character identified by the character or characters that immediately + * follow a backslash. The backslash '\' should have already been read. This supports + * both unicode escapes "u000A" and two-character escapes "\n". + * @return the unescaped char + * @throws NumberFormatException if any unicode escape sequences are malformed. + * @throws JSONException if processing of json failed + */ + private char readEscapeCharacter() throws JSONException { + char escaped = this.in.charAt(this.pos++); + switch (escaped) { + case 'u': + if (this.pos + 4 > this.in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = this.in.substring(this.pos, this.pos + 4); + this.pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'', '"', '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. Numeric values + * will be returned as an Integer, Long, or Double, in that order of preference. + * @return a literal value + * @throws JSONException if processing of json failed + */ + private Object readLiteral() throws JSONException { + String literal = nextToInternal("{}[]/\\:,=;# \t\f"); + + if (literal.isEmpty()) { + throw syntaxError("Expected literal value"); + } + else if ("null".equalsIgnoreCase(literal)) { + return JSONObject.NULL; + } + else if ("true".equalsIgnoreCase(literal)) { + return Boolean.TRUE; + } + else if ("false".equalsIgnoreCase(literal)) { + return Boolean.FALSE; + } + + /* try to parse as an integral type... */ + if (literal.indexOf('.') == -1) { + int base = 10; + String number = literal; + if (number.startsWith("0x") || number.startsWith("0X")) { + number = number.substring(2); + base = 16; + } + else if (number.startsWith("0") && number.length() > 1) { + number = number.substring(1); + base = 8; + } + try { + long longValue = Long.parseLong(number, base); + if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) { + return (int) longValue; + } + else { + return longValue; + } + } + catch (NumberFormatException e) { + /* + * This only happens for integral numbers greater than Long.MAX_VALUE, + * numbers in exponential form (5e-10) and unquoted strings. Fall through + * to try floating point. + */ + } + } + + /* ...next try to parse as a floating point... */ + try { + return Double.valueOf(literal); + } + catch (NumberFormatException ex) { + // Ignore + } + + /* ... finally give up. We have an unquoted string */ + return new String(literal); // a new string avoids leaking memory + } + + /** + * Returns the string up to but not including any of the given characters or a newline + * character. This does not consume the excluded character. + * @return the string up to but not including any of the given characters or a newline + * character + */ + private String nextToInternal(String excluded) { + int start = this.pos; + for (; this.pos < this.in.length(); this.pos++) { + char c = this.in.charAt(this.pos); + if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) { + return this.in.substring(start, this.pos); + } + } + return this.in.substring(start); + } + + /** + * Reads a sequence of key/value pairs and the trailing closing brace '}' of an + * object. The opening brace '{' should have already been read. + * @return an object + * @throws JSONException if processing of json failed + */ + private JSONObject readObject() throws JSONException { + JSONObject result = new JSONObject(); + + /* Peek to see if this is the empty object. */ + int first = nextCleanInternal(); + if (first == '}') { + return result; + } + else if (first != -1) { + this.pos--; + } + + while (true) { + Object name = nextValue(); + if (!(name instanceof String)) { + if (name == null) { + throw syntaxError("Names cannot be null"); + } + else { + throw syntaxError( + "Names must be strings, but " + name + " is of type " + name.getClass().getName()); + } + } + + /* + * Expect the name/value separator to be either a colon ':', an equals sign + * '=', or an arrow "=>". The last two are bogus but we include them because + * that's what the original implementation did. + */ + int separator = nextCleanInternal(); + if (separator != ':' && separator != '=') { + throw syntaxError("Expected ':' after " + name); + } + if (this.pos < this.in.length() && this.in.charAt(this.pos) == '>') { + this.pos++; + } + + result.put((String) name, nextValue()); + + switch (nextCleanInternal()) { + case '}': + return result; + case ';', ',': + continue; + default: + throw syntaxError("Unterminated object"); + } + } + } + + /** + * Reads a sequence of values and the trailing closing brace ']' of an array. The + * opening brace '[' should have already been read. Note that "[]" yields an empty + * array, but "[,]" returns a two-element array equivalent to "[null,null]". + * @return an array + * @throws JSONException if processing of json failed + */ + private JSONArray readArray() throws JSONException { + JSONArray result = new JSONArray(); + + /* to cover input that ends with ",]". */ + boolean hasTrailingSeparator = false; + + while (true) { + switch (nextCleanInternal()) { + case -1: + throw syntaxError("Unterminated array"); + case ']': + if (hasTrailingSeparator) { + result.put(null); + } + return result; + case ',', ';': + /* A separator without a value first means "null". */ + result.put(null); + hasTrailingSeparator = true; + continue; + default: + this.pos--; + } + + result.put(nextValue()); + + switch (nextCleanInternal()) { + case ']': + return result; + case ',', ';': + hasTrailingSeparator = true; + continue; + default: + throw syntaxError("Unterminated array"); + } + } + } + + /** + * Returns an exception containing the given message plus the current position and the + * entire input string. + * @param message the message + * @return an exception + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this); + } + + /** + * Returns the current position and the entire input string. + * @return the current position and the entire input string. + */ + @Override + public String toString() { + // consistent with the original implementation + return " at character " + this.pos + " of " + this.in; + } + + /* + * Legacy APIs. + * + * None of the methods below are on the critical path of parsing JSON documents. They + * exist only because they were exposed by the original implementation and may be used + * by some clients. + */ + + public boolean more() { + return this.pos < this.in.length(); + } + + public char next() { + return this.pos < this.in.length() ? this.in.charAt(this.pos++) : '\0'; + } + + public char next(char c) throws JSONException { + char result = next(); + if (result != c) { + throw syntaxError("Expected " + c + " but was " + result); + } + return result; + } + + public char nextClean() throws JSONException { + int nextCleanInt = nextCleanInternal(); + return nextCleanInt == -1 ? '\0' : (char) nextCleanInt; + } + + public String next(int length) throws JSONException { + if (this.pos + length > this.in.length()) { + throw syntaxError(length + " is out of bounds"); + } + String result = this.in.substring(this.pos, this.pos + length); + this.pos += length; + return result; + } + + public String nextTo(String excluded) { + if (excluded == null) { + throw new NullPointerException("excluded == null"); + } + return nextToInternal(excluded).trim(); + } + + public String nextTo(char excluded) { + return nextToInternal(String.valueOf(excluded)).trim(); + } + + public void skipPast(String thru) { + int thruStart = this.in.indexOf(thru, this.pos); + this.pos = thruStart == -1 ? this.in.length() : (thruStart + thru.length()); + } + + public char skipTo(char to) { + int index = this.in.indexOf(to, this.pos); + if (index != -1) { + this.pos = index; + return to; + } + else { + return '\0'; + } + } + + public void back() { + if (--this.pos == -1) { + this.pos = 0; + } + } + + public static int dehexchar(char hex) { + if (hex >= '0' && hex <= '9') { + return hex - '0'; + } + else if (hex >= 'A' && hex <= 'F') { + return hex - 'A' + 10; + } + else if (hex >= 'a' && hex <= 'f') { + return hex - 'a' + 10; + } + else { + return -1; + } + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java b/configuration-metadata/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java index d47f3563af8..e3820d7f8b4 100644 --- a/configuration-metadata/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java +++ b/configuration-metadata/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java @@ -25,8 +25,8 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; -import org.json.JSONArray; -import org.json.JSONObject; +import org.springframework.boot.configurationmetadata.json.JSONArray; +import org.springframework.boot.configurationmetadata.json.JSONObject; /** * Read standard json metadata format as {@link ConfigurationMetadataRepository}. diff --git a/configuration-metadata/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java b/configuration-metadata/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java index 3be4cc7e7ab..5b8b66172b4 100644 --- a/configuration-metadata/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java +++ b/configuration-metadata/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java @@ -21,9 +21,10 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; -import org.json.JSONException; import org.junit.jupiter.api.Test; +import org.springframework.boot.configurationmetadata.json.JSONException; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; diff --git a/platform/spring-boot-internal-dependencies/build.gradle b/platform/spring-boot-internal-dependencies/build.gradle index 0ab9383dfbb..54b72acde41 100644 --- a/platform/spring-boot-internal-dependencies/build.gradle +++ b/platform/spring-boot-internal-dependencies/build.gradle @@ -27,13 +27,6 @@ bom { issueLabels = ["type: task"] } } - library("Android JSON", "0.0.20131108.vaadin1") { - group("com.vaadin.external.google") { - modules = [ - "android-json" - ] - } - } library("API Guardian", "1.1.2") { group("org.apiguardian") { modules = [