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}