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 }