1 /*
  2  * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 package oracle.code.json;
 26 
 27 import oracle.code.json.impl.JsonParser;
 28 import oracle.code.json.impl.Utils;
 29 
 30 import java.math.BigDecimal;
 31 import java.math.BigInteger;
 32 import java.util.*;
 33 
 34 /**
 35  * This class provides static methods for producing and manipulating a {@link JsonValue}.
 36  * <p>
 37  * {@link #parse(String)} and {@link #parse(char[])} produce a {@code JsonValue}
 38  * by parsing data adhering to the JSON syntax defined in RFC 8259.
 39  * <p>
 40  * {@link #toDisplayString(JsonValue)} is a formatter that produces a
 41  * representation of the JSON value suitable for display.
 42  * <p>
 43  * {@link #fromUntyped(Object)} and {@link #toUntyped(JsonValue)} provide a conversion
 44  * between {@code JsonValue} and an untyped object.
 45  *
 46  * @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript
 47  *      Object Notation (JSON) Data Interchange Format
 48  * @since 99
 49  */
 50 public final class Json {
 51 
 52     /**
 53      * Parses and creates a {@code JsonValue} from the given JSON document.
 54      * If parsing succeeds, it guarantees that the input document conforms to
 55      * the JSON syntax. If the document contains any JSON Object that has
 56      * duplicate names, a {@code JsonParseException} is thrown.
 57      * <p>
 58      * {@code JsonValue}s created by this method produce their String and underlying
 59      * value representation lazily.
 60      * <p>
 61      * {@code JsonObject}s preserve the order of their members declared in and parsed from
 62      * the JSON document.
 63      *
 64      * @param in the input JSON document as {@code String}. Non-null.
 65      * @throws JsonParseException if the input JSON document does not conform
 66      *      to the JSON document format or a JSON object containing
 67      *      duplicate names is encountered.
 68      * @throws NullPointerException if {@code in} is {@code null}
 69      * @return the parsed {@code JsonValue}
 70      */
 71     public static JsonValue parse(String in) {
 72         Objects.requireNonNull(in);
 73         return new JsonParser(in.toCharArray()).parseRoot();
 74     }
 75 
 76     /**
 77      * Parses and creates a {@code JsonValue} from the given JSON document.
 78      * If parsing succeeds, it guarantees that the input document conforms to
 79      * the JSON syntax. If the document contains any JSON Object that has
 80      * duplicate names, a {@code JsonParseException} is thrown.
 81      * <p>
 82      * {@code JsonValue}s created by this method produce their String and underlying
 83      * value representation lazily.
 84      * <p>
 85      * {@code JsonObject}s preserve the order of their members declared in and parsed from
 86      * the JSON document.
 87      *
 88      * @param in the input JSON document as {@code char[]}. Non-null.
 89      * @throws JsonParseException if the input JSON document does not conform
 90      *      to the JSON document format or a JSON object containing
 91      *      duplicate names is encountered.
 92      * @throws NullPointerException if {@code in} is {@code null}
 93      * @return the parsed {@code JsonValue}
 94      */
 95     public static JsonValue parse(char[] in) {
 96         Objects.requireNonNull(in);
 97         return new JsonParser(Arrays.copyOf(in, in.length)).parseRoot();
 98     }
 99 
100     /**
101      * {@return a {@code JsonValue} created from the given {@code src} object}
102      * The mapping from an untyped {@code src} object to a {@code JsonValue}
103      * follows the table below.
104      * <table class="striped">
105      * <caption>Untyped to JsonValue mapping</caption>
106      * <thead>
107      *    <tr>
108      *       <th scope="col" class="TableHeadingColor">Untyped Object</th>
109      *       <th scope="col" class="TableHeadingColor">JsonValue</th>
110      *    </tr>
111      * </thead>
112      * <tbody>
113      * <tr>
114      *     <th>{@code List<Object>}</th>
115      *     <th>{@code JsonArray}</th>
116      * </tr>
117      * <tr>
118      *     <th>{@code Boolean}</th>
119      *     <th>{@code JsonBoolean}</th>
120      * </tr>
121      * <tr>
122      *     <th>{@code `null`}</th>
123      *     <th>{@code JsonNull}</th>
124      * </tr>
125      * <tr>
126      *     <th>{@code Number*}</th>
127      *     <th>{@code JsonNumber}</th>
128      * </tr>
129      * <tr>
130      *     <th>{@code Map<String, Object>}</th>
131      *     <th>{@code JsonObject}</th>
132      * </tr>
133      * <tr>
134      *     <th>{@code String}</th>
135      *     <th>{@code JsonString}</th>
136      * </tr>
137      * </tbody>
138      * </table>
139      *
140      * <i><sup>*</sup>The supported {@code Number} subclasses are: {@code Byte},
141      * {@code Short}, {@code Integer}, {@code Long}, {@code Float},
142      * {@code Double}, {@code BigInteger}, and {@code BigDecimal}.</i>
143      *
144      * <p>If {@code src} is an instance of {@code JsonValue}, it is returned as is.
145      * If {@code src} contains a circular reference, {@code IllegalArgumentException}
146      * will be thrown. For example, the following code throws an exception,
147      * {@snippet lang=java:
148      *     var map = new HashMap<String, Object>();
149      *     map.put("foo", false);
150      *     map.put("bar", map);
151      *     Json.fromUntyped(map);
152      * }
153      *
154      * @param src the data to produce the {@code JsonValue} from. May be null.
155      * @throws IllegalArgumentException if {@code src} cannot be converted
156      *      to {@code JsonValue} or contains a circular reference.
157      * @see #toUntyped(JsonValue)
158      */
159     public static JsonValue fromUntyped(Object src) {
160         return fromUntyped(src, Collections.newSetFromMap(new IdentityHashMap<>()));
161     }
162 
163     static JsonValue fromUntyped(Object src, Set<Object> identitySet) {
164         return switch (src) {
165             // Structural: JSON object, JSON array
166             case Map<?, ?> map -> {
167                 if (!identitySet.add(map)) {
168                     throw new IllegalArgumentException("Circular reference detected");
169                 }
170                 Map<String, JsonValue> m = LinkedHashMap.newLinkedHashMap(map.size());
171                 for (Map.Entry<?, ?> entry : new LinkedHashMap<>(map).entrySet()) {
172                     if (!(entry.getKey() instanceof String strKey)) {
173                         throw new IllegalArgumentException("Key is not a String: " + entry.getKey());
174                     } else {
175                         var unescapedKey = Utils.unescape(
176                                 strKey.toCharArray(), 0, strKey.length());
177                         if (m.containsKey(unescapedKey)) {
178                             throw new IllegalArgumentException(
179                                     "Duplicate member name: '%s'".formatted(unescapedKey));
180                         } else {
181                             m.put(unescapedKey, Json.fromUntyped(entry.getValue(), identitySet));
182                         }
183                     }
184                 }
185                 // Bypasses defensive copy in JsonObject.of(m)
186                 yield Utils.objectOf(m);
187             }
188             case List<?> list -> {
189                 if (!identitySet.add(list)) {
190                     throw new IllegalArgumentException("Circular reference detected");
191                 }
192                 List<JsonValue> l = new ArrayList<>(list.size());
193                 for (Object o : list) {
194                     l.add(Json.fromUntyped(o, identitySet));
195                 }
196                 // Bypasses defensive copy in JsonArray.of(l)
197                 yield Utils.arrayOf(l);
198             }
199             // JSON primitives
200             case String str -> JsonString.of(str);
201             case Boolean bool -> JsonBoolean.of(bool);
202             case Byte b -> JsonNumber.of(b);
203             case Integer i -> JsonNumber.of(i);
204             case Long l -> JsonNumber.of(l);
205             case Short s -> JsonNumber.of(s);
206             case Float f -> JsonNumber.of(f);
207             case Double d -> JsonNumber.of(d);
208             case BigInteger bi -> JsonNumber.of(bi);
209             case BigDecimal bd -> JsonNumber.of(bd);
210             case null -> JsonNull.of();
211             // JsonValue
212             case JsonValue jv -> jv;
213             default -> throw new IllegalArgumentException("Type not recognized.");
214         };
215     }
216 
217     /**
218      * {@return an {@code Object} created from the given {@code src}
219      * {@code JsonValue}} The mapping from a {@code JsonValue} to an
220      * untyped {@code src} object follows the table below.
221      * <table class="striped">
222      * <caption>JsonValue to Untyped mapping</caption>
223      * <thead>
224      *    <tr>
225      *       <th scope="col" class="TableHeadingColor">JsonValue</th>
226      *       <th scope="col" class="TableHeadingColor">Untyped Object</th>
227      *    </tr>
228      * </thead>
229      * <tbody>
230      * <tr>
231      *     <th>{@code JsonArray}</th>
232      *     <th>{@code List<Object>}(unmodifiable)</th>
233      * </tr>
234      * <tr>
235      *     <th>{@code JsonBoolean}</th>
236      *     <th>{@code Boolean}</th>
237      * </tr>
238      * <tr>
239      *     <th>{@code JsonNull}</th>
240      *     <th>{@code `null`}</th>
241      * </tr>
242      * <tr>
243      *     <th>{@code JsonNumber}</th>
244      *     <th>{@code Number}</th>
245      * </tr>
246      * <tr>
247      *     <th>{@code JsonObject}</th>
248      *     <th>{@code Map<String, Object>}(unmodifiable)</th>
249      * </tr>
250      * <tr>
251      *     <th>{@code JsonString}</th>
252      *     <th>{@code String}</th>
253      * </tr>
254      * </tbody>
255      * </table>
256      *
257      * <p>
258      * A {@code JsonObject} in {@code src} is converted to a {@code Map} whose
259      * entries occur in the same order as the {@code JsonObject}'s members.
260      *
261      * @param src the {@code JsonValue} to convert to untyped. Non-null.
262      * @throws NullPointerException if {@code src} is {@code null}
263      * @see #fromUntyped(Object)
264      */
265     public static Object toUntyped(JsonValue src) {
266         Objects.requireNonNull(src);
267         return switch (src) {
268             case JsonObject jo -> jo.members().entrySet().stream()
269                     .collect(LinkedHashMap::new, // to allow `null` value
270                             (m, e) -> m.put(e.getKey(), Json.toUntyped(e.getValue())),
271                             HashMap::putAll);
272             case JsonArray ja -> ja.values().stream()
273                     .map(Json::toUntyped)
274                     .toList();
275             case JsonBoolean jb -> jb.value();
276             case JsonNull _ -> null;
277             case JsonNumber n -> n.toNumber();
278             case JsonString js -> js.value();
279         };
280     }
281 
282     /**
283      * {@return the String representation of the given {@code JsonValue} that conforms
284      * to the JSON syntax} As opposed to the compact output returned by {@link
285      * JsonValue#toString()}, this method returns a JSON string that is better
286      * suited for display.
287      *
288      * @param value the {@code JsonValue} to create the display string from. Non-null.
289      * @throws NullPointerException if {@code value} is {@code null}
290      * @see JsonValue#toString()
291      */
292     public static String toDisplayString(JsonValue value) {
293         Objects.requireNonNull(value);
294         return toDisplayString(value, 0 , false);
295     }
296 
297     private static String toDisplayString(JsonValue jv, int indent, boolean isField) {
298         return switch (jv) {
299             case JsonObject jo -> toDisplayString(jo, indent, isField);
300             case JsonArray ja -> toDisplayString(ja, indent, isField);
301             default -> " ".repeat(isField ? 1 : indent) + jv;
302         };
303     }
304 
305     private static String toDisplayString(JsonObject jo, int indent, boolean isField) {
306         var prefix = " ".repeat(indent);
307         var s = new StringBuilder(isField ? " " : prefix);
308         if (jo.members().isEmpty()) {
309             s.append("{}");
310         } else {
311             s.append("{\n");
312             jo.members().forEach((name, value) -> {
313                 if (value instanceof JsonValue val) {
314                     s.append(prefix)
315                             .append(" ".repeat(INDENT))
316                             .append("\"")
317                             .append(name)
318                             .append("\":")
319                             .append(Json.toDisplayString(val, indent + INDENT, true))
320                             .append(",\n");
321                 } else {
322                     throw new InternalError("type mismatch");
323                 }
324             });
325             s.setLength(s.length() - 2); // trim final comma
326             s.append("\n").append(prefix).append("}");
327         }
328         return s.toString();
329     }
330 
331     private static String toDisplayString(JsonArray ja, int indent, boolean isField) {
332         var prefix = " ".repeat(indent);
333         var s = new StringBuilder(isField ? " " : prefix);
334         if (ja.values().isEmpty()) {
335             s.append("[]");
336         } else {
337             s.append("[\n");
338             for (JsonValue v: ja.values()) {
339                 if (v instanceof JsonValue jv) {
340                     s.append(Json.toDisplayString(jv,indent + INDENT, false)).append(",\n");
341                 } else {
342                     throw new InternalError("type mismatch");
343                 }
344             }
345             s.setLength(s.length() - 2); // trim final comma/newline
346             s.append("\n").append(prefix).append("]");
347         }
348         return s.toString();
349     }
350 
351     // default indentation for display string
352     private static final int INDENT = 2;
353 
354     // no instantiation is allowed for this class
355     private Json() {}
356 }