001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.json5; 018 019import static java.util.Collections.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.util.*; 024import java.util.function.*; 025 026import org.apache.juneau.marshall.*; 027import org.apache.juneau.marshall.collections.*; 028import org.apache.juneau.marshall.marshaller.*; 029import org.apache.juneau.marshall.parser.*; 030import org.apache.juneau.marshall.serializer.*; 031 032/** 033 * JSON5-flavored {@link MarshalledMap}. 034 * 035 * <p> 036 * Carries the JSON5-specific surface that historically lived on {@link JsonMap}: a JSON5 037 * {@link #toString()}, JSON5 default-parser constructors, and convenience methods for working with 038 * JSON5 text. Use this class when you want a {@code Map}-shaped value whose textual form (and 039 * default parsing) is JSON5 ("simplified JSON" with unquoted keys, single-quoted strings, etc.). 040 * 041 * <p> 042 * For strict (RFC 8259) JSON behavior, use {@link JsonMap}. For a neutral, marshaller-agnostic 043 * base with no language coupling, use {@link MarshalledMap}. 044 * 045 * <h5 class='section'>Example:</h5> 046 * <p class='bjava'> 047 * <jc>// Construct an empty Map</jc> 048 * Json5Map <jv>map</jv> = Json5Map.<jsm>of</jsm>(); 049 * 050 * <jc>// Construct a Map from JSON5</jc> 051 * <jv>map</jv> = Json5Map.<jsm>ofString</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>); 052 * 053 * <jc>// JSON5 round-trip via toString()</jc> 054 * String <jv>s</jv> = <jv>map</jv>.toString(); <jc>// "{a:'A',b:{c:'C',d:123}}"</jc> 055 * </p> 056 * 057 * <h5 class='section'>Notes:</h5><ul> 058 * <li class='warn'>This class is not thread safe. 059 * </ul> 060 */ 061@SuppressWarnings({ 062 "java:S110", // Inheritance depth comes from the inherited MarshalledMap/LinkedHashMap hierarchy, not from this class 063 "java:S2160", // equals() / hashCode() inherited from MarshalledMap; map equality is element-based 064 "java:S9149" // Per-format static factories intentionally shadow the parent's. 065}) 066public class Json5Map extends MarshalledMap { 067 068 private static class Unmodifiable extends Json5Map { 069 private static final long serialVersionUID = 1L; 070 071 @SuppressWarnings({ 072 "synthetic-access" // Access to outer class members is intentional 073 }) 074 Unmodifiable(Json5Map contents) { 075 if (nn(contents)) 076 contents.forEach(super::put); 077 } 078 079 @Override 080 public void clear() { 081 throw uoroex(); 082 } 083 084 @Override 085 public Object compute(String key, BiFunction<? super String,? super Object,?> remappingFunction) { 086 throw uoroex(); 087 } 088 089 @Override 090 public Object computeIfAbsent(String key, Function<? super String,?> mappingFunction) { 091 throw uoroex(); 092 } 093 094 @Override 095 public Object computeIfPresent(String key, BiFunction<? super String,? super Object,?> remappingFunction) { 096 throw uoroex(); 097 } 098 099 @Override 100 public Set<Map.Entry<String,Object>> entrySet() { 101 return u(super.entrySet()); 102 } 103 104 @Override 105 public boolean isUnmodifiable() { return true; } 106 107 @Override 108 public Set<String> keySet() { 109 return u(super.keySet()); 110 } 111 112 @Override 113 public Object merge(String key, Object value, BiFunction<? super Object,? super Object,?> remappingFunction) { 114 throw uoroex(); 115 } 116 117 @Override 118 public Object put(String key, Object val) { 119 throw uoroex(); 120 } 121 122 @Override 123 public void putAll(Map<? extends String,?> m) { 124 throw uoroex(); 125 } 126 127 @Override 128 public Object putIfAbsent(String key, Object value) { 129 throw uoroex(); 130 } 131 132 @Override 133 public Object remove(Object key) { 134 throw uoroex(); 135 } 136 137 @Override 138 public boolean remove(Object key, Object value) { 139 throw uoroex(); 140 } 141 142 @Override 143 public Object replace(String key, Object value) { 144 throw uoroex(); 145 } 146 147 @Override 148 public boolean replace(String key, Object oldValue, Object newValue) { 149 throw uoroex(); 150 } 151 152 @Override 153 public void replaceAll(BiFunction<? super String,? super Object,?> function) { 154 throw uoroex(); 155 } 156 157 @Override 158 public Collection<Object> values() { 159 return Collections.unmodifiableCollection(super.values()); 160 } 161 } 162 163 private static final long serialVersionUID = 1L; 164 165 /** 166 * An empty read-only Json5Map. 167 * 168 * @serial exclude 169 */ 170 @SuppressWarnings({ 171 "java:S2386" // Public static final field accessed externally, cannot be protected 172 }) 173 public static final Json5Map EMPTY_MAP = new Json5Map() { 174 175 private static final long serialVersionUID = 1L; 176 177 @Override /* Overridden from Map */ 178 public Set<Map.Entry<String,Object>> entrySet() { 179 return Collections.<String,Object>emptyMap().entrySet(); 180 } 181 182 @Override /* Overridden from Map */ 183 public Set<String> keySet() { 184 return Collections.<String,Object>emptyMap().keySet(); 185 } 186 187 @Override /* Overridden from Map */ 188 public Object put(String key, Object value) { 189 throw uoroex(); 190 } 191 192 @Override /* Overridden from Map */ 193 public Object remove(Object key) { 194 throw uoroex(); 195 } 196 197 @Override /* Overridden from Map */ 198 public Collection<Object> values() { 199 return emptyMap().values(); 200 } 201 }; 202 203 /** 204 * Construct an empty map. 205 * 206 * @return An empty map. 207 */ 208 public static Json5Map create() { 209 return new Json5Map(); 210 } 211 212 /** 213 * Construct an empty filtered map. 214 * 215 * @return An empty map. 216 */ 217 public static Json5Map filteredMap() { 218 return create().filtered(); 219 } 220 221 /** 222 * Construct a filtered map initialized with the specified key/value pairs. 223 * 224 * <p> 225 * Same as {@link #of(Object...)} but calls {@link #filtered()} on the created map. 226 * 227 * @param keyValuePairs A list of key/value pairs to add to this map. 228 * @return A new map, never <jk>null</jk>. 229 */ 230 public static Json5Map filteredMap(Object...keyValuePairs) { 231 return new Json5Map(keyValuePairs).filtered(); 232 } 233 234 /** 235 * Construct a map initialized with the specified map. 236 * 237 * @param values 238 * The map to copy. 239 * <br>Can be <jk>null</jk> (returns <jk>null</jk>). 240 * <br>Keys will be converted to strings using {@link Object#toString()}. 241 * @return A new map or <jk>null</jk> if the map was <jk>null</jk>. 242 */ 243 public static Json5Map of(Map<?,?> values) { 244 return values == null ? null : new Json5Map(values); 245 } 246 247 /** 248 * Construct a map initialized with the specified key/value pairs. 249 * 250 * <h5 class='section'>Examples:</h5> 251 * <p class='bjava'> 252 * Json5Map <jv>map</jv> = <jk>new</jk> Json5Map(<js>"key1"</js>,<js>"val1"</js>,<js>"key2"</js>,<js>"val2"</js>); 253 * </p> 254 * 255 * @param keyValuePairs A list of key/value pairs to add to this map. 256 * @return A new map, never <jk>null</jk>. 257 */ 258 public static Json5Map of(Object...keyValuePairs) { 259 return new Json5Map(keyValuePairs); 260 } 261 262 /** 263 * Construct a map initialized with the specified JSON5 string. 264 * 265 * @param json5 266 * The JSON5 text to parse. 267 * @return A new map (empty if the input was <jk>null</jk>), never <jk>null</jk>. 268 * @throws ParseException Malformed input encountered. 269 */ 270 public static Json5Map ofString(CharSequence json5) throws ParseException { 271 return json5 == null ? new Json5Map() : new Json5Map(json5); 272 } 273 274 /** 275 * Construct a map initialized with the specified reader containing JSON5. 276 * 277 * @param json5 278 * The reader containing JSON5 text to parse. 279 * @return A new map (empty if the input was <jk>null</jk>), never <jk>null</jk>. 280 * @throws ParseException Malformed input encountered. 281 */ 282 public static Json5Map ofString(Reader json5) throws ParseException { 283 return json5 == null ? new Json5Map() : new Json5Map(json5); 284 } 285 286 /** 287 * Construct a map initialized with the specified string and parser. 288 * 289 * @param in The input being parsed. 290 * @param p 291 * The parser to use to parse the input. 292 * <br>If <jk>null</jk>, uses {@link Json5Parser}. 293 * @return A new map (empty if the input was <jk>null</jk>), never <jk>null</jk>. 294 * @throws ParseException Malformed input encountered. 295 */ 296 public static Json5Map ofString(CharSequence in, Parser p) throws ParseException { 297 return in == null ? new Json5Map() : new Json5Map(in, p); 298 } 299 300 /** 301 * Construct a map initialized with the specified reader and parser. 302 * 303 * @param in 304 * The reader containing the input being parsed. 305 * @param p 306 * The parser to use to parse the input. 307 * <br>If <jk>null</jk>, uses {@link Json5Parser}. 308 * @return A new map (empty if the input was <jk>null</jk>), never <jk>null</jk>. 309 * @throws ParseException Malformed input encountered. 310 */ 311 public static Json5Map ofString(Reader in, Parser p) throws ParseException { 312 return in == null ? new Json5Map() : new Json5Map(in, p); 313 } 314 315 /** 316 * Construct an empty map. 317 */ 318 public Json5Map() {} 319 320 /** 321 * Construct an empty map with the specified bean context. 322 * 323 * @param session The bean session to use for creating beans. 324 */ 325 public Json5Map(MarshallingSession session) { 326 super(session); 327 } 328 329 /** 330 * Construct a map initialized with the specified JSON5 text. 331 * 332 * @param json5 The JSON5 text to parse. 333 * @throws ParseException Malformed input encountered. 334 */ 335 public Json5Map(CharSequence json5) throws ParseException { 336 this(json5, Json5Parser.DEFAULT); 337 } 338 339 /** 340 * Construct a map initialized with the specified string. 341 * 342 * @param in The input being parsed. Can be <jk>null</jk> or empty (results in an empty map; no parsing occurs). 343 * @param p The parser to use. If <jk>null</jk>, uses {@link Json5Parser}. 344 * @throws ParseException Malformed input encountered. 345 */ 346 public Json5Map(CharSequence in, Parser p) throws ParseException { 347 super(in, p == null ? Json5Parser.DEFAULT : p); 348 } 349 350 /** 351 * Construct a map initialized with the specified map. 352 * 353 * @param in 354 * The map to copy. 355 * <br>Can be <jk>null</jk> (results in an empty map). 356 * <br>Keys will be converted to strings using {@link Object#toString()}. 357 */ 358 public Json5Map(Map<?,?> in) { 359 super(in); 360 } 361 362 /** 363 * Construct a map initialized with the specified key/value pairs. 364 * 365 * @param keyValuePairs A list of key/value pairs to add to this map. 366 */ 367 public Json5Map(Object...keyValuePairs) { 368 super(keyValuePairs); 369 } 370 371 /** 372 * Construct a map initialized with the specified reader containing JSON5. 373 * 374 * @param json5 The reader containing JSON5 text to parse. 375 * @throws ParseException Malformed input encountered. 376 */ 377 public Json5Map(Reader json5) throws ParseException { 378 this(json5, Json5Parser.DEFAULT); 379 } 380 381 /** 382 * Construct a map initialized with the specified reader and parser. 383 * 384 * @param in The reader containing the input being parsed. 385 * @param p The parser to use. If <jk>null</jk>, uses {@link Json5Parser}. 386 * @throws ParseException Malformed input encountered. 387 */ 388 public Json5Map(Reader in, Parser p) throws ParseException { 389 super(in, p == null ? Json5Parser.DEFAULT : p); 390 } 391 392 @Override /* Overridden from MarshalledMap */ 393 public Json5Map append(Map<String,Object> values) { 394 super.append(values); 395 return this; 396 } 397 398 @Override /* Overridden from MarshalledMap */ 399 public Json5Map append(String key, Object value) { 400 super.append(key, value); 401 return this; 402 } 403 404 @Override /* Overridden from MarshalledMap */ 405 @SuppressWarnings({ 406 "unchecked", // Generic varargs is safe here: values are only iterated, not stored into a typed array. 407 "varargs" // appendFirst()'s T... values is only forwarded to super.appendFirst(), never stored, so heap pollution cannot occur 408 }) 409 public <T> Json5Map appendFirst(Predicate<T> test, String key, T...values) { 410 super.appendFirst(test, key, values); 411 return this; 412 } 413 414 @Override /* Overridden from MarshalledMap */ 415 public Json5Map appendIf(boolean flag, String key, Object value) { 416 super.appendIf(flag, key, value); 417 return this; 418 } 419 420 @Override /* Overridden from MarshalledMap */ 421 public <T> Json5Map appendIf(Predicate<T> test, String key, T value) { 422 super.appendIf(test, key, value); 423 return this; 424 } 425 426 @Override /* Overridden from MarshalledMap */ 427 public Json5Map appendIfAbsent(String key, Object value) { 428 super.appendIfAbsent(key, value); 429 return this; 430 } 431 432 @Override /* Overridden from MarshalledMap */ 433 public <T> Json5Map appendIfAbsentIf(Predicate<T> predicate, String key, T value) { 434 super.appendIfAbsentIf(predicate, key, value); 435 return this; 436 } 437 438 @Override /* Overridden from MarshalledMap */ 439 public Json5Map exclude(String...keys) { 440 var m2 = new Json5Map(); 441 this.forEach((k, v) -> { 442 var exclude = false; 443 for (var kk : keys) 444 if (kk.equals(k)) 445 exclude = true; 446 if (! exclude) 447 m2.put(k, v); 448 }); 449 return m2; 450 } 451 452 @Override /* Overridden from MarshalledMap */ 453 public Json5Map filtered() { 454 super.filtered(); 455 return this; 456 } 457 458 @Override /* Overridden from MarshalledMap */ 459 public Json5Map filtered(Predicate<Object> value) { 460 super.filtered(value); 461 return this; 462 } 463 464 @Override /* Overridden from MarshalledMap */ 465 public Json5List findList(String...keys) { 466 return find(Json5List.class, keys); 467 } 468 469 @Override /* Overridden from MarshalledMap */ 470 public Json5Map findMap(String...keys) { 471 return find(Json5Map.class, keys); 472 } 473 474 @Override /* Overridden from MarshalledMap */ 475 public Json5List getList(String key) { 476 return get(key, Json5List.class); 477 } 478 479 @Override /* Overridden from MarshalledMap */ 480 public Json5List getList(String key, boolean createIfNotExists) { 481 Json5List m = getWithDefault(key, null, Json5List.class); 482 if (m == null && createIfNotExists) { 483 m = new Json5List(); 484 put(key, m); 485 } 486 return m; 487 } 488 489 /** 490 * Returns the specified entry value converted to a {@link Json5List}. 491 * 492 * @param key The key. 493 * @param defVal The default value if the map doesn't contain the specified mapping. 494 * @return The converted value, or the default value if the map contains no mapping for this key. 495 * @throws InvalidDataConversionException If value cannot be converted. 496 */ 497 public Json5List getList(String key, Json5List defVal) { 498 return getWithDefault(key, defVal, Json5List.class); 499 } 500 501 @Override /* Overridden from MarshalledMap */ 502 public Json5Map getMap(String key) { 503 return get(key, Json5Map.class); 504 } 505 506 @Override /* Overridden from MarshalledMap */ 507 public Json5Map getMap(String key, boolean createIfNotExists) { 508 var m = getWithDefault(key, null, Json5Map.class); 509 if (m == null && createIfNotExists) { 510 m = new Json5Map(); 511 put(key, m); 512 } 513 return m; 514 } 515 516 /** 517 * Returns the specified entry value converted to a {@link Json5Map}. 518 * 519 * @param key The key. 520 * @param defVal The default value if the map doesn't contain the specified mapping. 521 * @return The converted value, or the default value if the map contains no mapping for this key. 522 * @throws InvalidDataConversionException If value cannot be converted. 523 */ 524 public Json5Map getMap(String key, Json5Map defVal) { 525 return getWithDefault(key, defVal, Json5Map.class); 526 } 527 528 @Override /* Overridden from MarshalledMap */ 529 public Json5Map include(String...keys) { 530 var m2 = new Json5Map(); 531 this.forEach((k, v) -> { 532 for (var kk : keys) 533 if (kk.equals(k)) 534 m2.put(kk, v); 535 }); 536 return m2; 537 } 538 539 @Override /* Overridden from MarshalledMap */ 540 public Json5Map inner(Map<String,Object> inner) { 541 super.inner(inner); 542 return this; 543 } 544 545 @Override /* Overridden from MarshalledMap */ 546 public Json5Map keepAll(String...keys) { 547 super.keepAll(keys); 548 return this; 549 } 550 551 @Override /* Overridden from MarshalledMap */ 552 public Json5Map modifiable() { 553 if (isUnmodifiable()) 554 return new Json5Map(this); 555 return this; 556 } 557 558 @Override /* Overridden from MarshalledMap */ 559 protected Json5Map newMarshalledMap(MarshallingSession bs) { 560 return new Json5Map(bs); 561 } 562 563 /** 564 * Convenience method for inserting JSON5 directly into an attribute on this object. 565 * 566 * <p> 567 * The JSON5 text can be an object (i.e. <js>"{...}"</js>) or an array (i.e. <js>"[...]"</js>). 568 * 569 * @param key The key. 570 * @param json5 The JSON5 text that will be parsed into an Object and then inserted into this map. 571 * @throws ParseException Malformed input encountered. 572 */ 573 public void putJson5(String key, String json5) throws ParseException { 574 this.put(key, Json5.to(json5, Object.class)); 575 } 576 577 @Override /* Overridden from MarshalledMap */ 578 public Json5Map session(MarshallingSession session) { 579 super.session(session); 580 return this; 581 } 582 583 @Override /* Overridden from MarshalledMap */ 584 public Json5Map setBeanSession(MarshallingSession value) { 585 super.setBeanSession(value); 586 return this; 587 } 588 589 /** 590 * Serializes this map to a JSON5 string. 591 * 592 * <p> 593 * A synonym for {@link #toString()}. 594 * 595 * @return This object as a JSON5 string. 596 */ 597 public String toJson5() { 598 return Json5.of(this); 599 } 600 601 /** 602 * Serializes this map to a readable (indented) JSON5 string using {@link Json5Serializer#DEFAULT_READABLE}. 603 * 604 * @return This object serialized as a readable JSON5 string. 605 */ 606 public String toReadableJson5() { 607 if (Json5Serializer.DEFAULT_READABLE == null) 608 return s(this); 609 return Json5Serializer.DEFAULT_READABLE.toString(this); 610 } 611 612 @Override /* Overridden from Object */ 613 public String toString() { 614 return Json5.of(this); 615 } 616 617 @Override /* Overridden from MarshalledMap */ 618 public Json5Map unmodifiable() { 619 if (this instanceof Unmodifiable this2) 620 return this2; 621 return new Unmodifiable(this); 622 } 623 624 /** 625 * Convenience method for serializing this map to the specified <c>Writer</c> using the 626 * {@link Json5#DEFAULT} marshaller. 627 * 628 * @param w The writer to serialize this object to. 629 * @return This object. 630 * @throws IOException If a problem occurred trying to write to the writer. 631 * @throws SerializeException If a problem occurred trying to convert the output. 632 */ 633 public Json5Map writeTo(Writer w) throws IOException, SerializeException { 634 Json5.DEFAULT.write(this, w); 635 return this; 636 } 637}