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 }