001/*
002 * Copyright (C) 2010 The Android Open Source Project
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.apache.tapestry5.json;
018
019import java.io.ObjectStreamException;
020import java.io.Serializable;
021import java.util.Collections;
022import java.util.LinkedHashMap;
023import java.util.Map;
024import java.util.Set;
025
026// Note: this class was written without inspecting the non-free org.json sourcecode.
027
028/**
029 * A modifiable set of name/value mappings. Names are unique, non-null strings.
030 * Values may be any mix of {@link JSONObject JSONObjects}, {@link JSONArray
031 * JSONArrays}, Strings, Booleans, Integers, Longs, Doubles or {@link #NULL}.
032 * Values may not be {@code null}, {@link Double#isNaN() NaNs}, {@link
033 * Double#isInfinite() infinities}, or of any type not listed here.
034 *
035 * <p>This class can coerce values to another type when requested.
036 * <ul>
037 * <li>When the requested type is a boolean, strings will be coerced using a
038 * case-insensitive comparison to "true" and "false".
039 * <li>When the requested type is a double, other {@link Number} types will
040 * be coerced using {@link Number#doubleValue() doubleValue}. Strings
041 * that can be coerced using {@link Double#valueOf(String)} will be.
042 * <li>When the requested type is an int, other {@link Number} types will
043 * be coerced using {@link Number#intValue() intValue}. Strings
044 * that can be coerced using {@link Double#valueOf(String)} will be,
045 * and then cast to int.
046 * <li><a name="lossy">When the requested type is a long, other {@link Number} types will
047 * be coerced using {@link Number#longValue() longValue}. Strings
048 * that can be coerced using {@link Double#valueOf(String)} will be,
049 * and then cast to long. This two-step conversion is lossy for very
050 * large values. For example, the string "9223372036854775806" yields the
051 * long 9223372036854775807.</a>
052 * <li>When the requested type is a String, other non-null values will be
053 * coerced using {@link String#valueOf(Object)}. Although null cannot be
054 * coerced, the sentinel value {@link JSONObject#NULL} is coerced to the
055 * string "null".
056 * </ul>
057 *
058 * <p>This class can look up both mandatory and optional values:
059 * <ul>
060 * <li>Use <code>get<i>Type</i>()</code> to retrieve a mandatory value. This
061 * fails with a {@code RuntimeException} if the requested name has no value
062 * or if the value cannot be coerced to the requested type.
063 * <li>Use <code>opt()</code> to retrieve an optional value.
064 * </ul>
065 *
066 * <p><strong>Warning:</strong> this class represents null in two incompatible
067 * ways: the standard Java {@code null} reference, and the sentinel value {@link
068 * JSONObject#NULL}. In particular, calling {@code put(name, null)} removes the
069 * named entry from the object but {@code put(name, JSONObject.NULL)} stores an
070 * entry whose value is {@code JSONObject.NULL}.
071 *
072 * <p>Instances of this class are not thread safe.
073 */
074public final class JSONObject extends JSONCollection {
075
076    private static final Double NEGATIVE_ZERO = -0d;
077
078    /**
079     * A sentinel value used to explicitly define a name with no value. Unlike
080     * {@code null}, names with this value:
081     * <ul>
082     * <li>show up in the {@link #names} array
083     * <li>show up in the {@link #keys} iterator
084     * <li>return {@code true} for {@link #has(String)}
085     * <li>do not throw on {@link #get(String)}
086     * <li>are included in the encoded JSON string.
087     * </ul>
088     *
089     * <p>This value violates the general contract of {@link Object#equals} by
090     * returning true when compared to {@code null}. Its {@link #toString}
091     * method returns "null".
092     */
093    public static final Object NULL = new Serializable() {
094
095      private static final long serialVersionUID = 1L;
096
097        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
098        @Override
099        public boolean equals(Object o) {
100            return o == this || o == null; // API specifies this broken equals implementation
101        }
102
103        // at least make the broken equals(null) consistent with Objects.hashCode(null).
104        @Override
105        public int hashCode() {
106            return 0;
107        }
108
109        @Override
110        public String toString() {
111            return "null";
112        }
113
114        // Serialization magic: after de-serializing, it will be back to the singleton instance of NULL.
115        private Object readResolve() throws ObjectStreamException
116        {
117            return NULL;
118        }
119
120    };
121
122    private final LinkedHashMap<String, Object> nameValuePairs;
123
124    /**
125     * Creates a {@code JSONObject} with no name/value mappings.
126     */
127    public JSONObject() {
128        nameValuePairs = new LinkedHashMap<String, Object>();
129    }
130
131    /**
132     * Creates a new {@code JSONObject} with name/value mappings from the next
133     * object in the tokener.
134     *
135     * @param readFrom a tokener whose nextValue() method will yield a
136     *                 {@code JSONObject}.
137     * @throws RuntimeException if the parse fails or doesn't yield a
138     *                       {@code JSONObject}.
139     */
140    JSONObject(JSONTokener readFrom) {
141        /*
142         * Getting the parser to populate this could get tricky. Instead, just
143         * parse to temporary JSONObject and then steal the data from that.
144         */
145        Object object = readFrom.nextValue(JSONObject.class);
146        if (object instanceof JSONObject) {
147            this.nameValuePairs = ((JSONObject) object).nameValuePairs;
148        } else {
149            throw JSON.typeMismatch(object, "JSONObject");
150        }
151    }
152
153    /**
154     * Creates a new {@code JSONObject} with name/value mappings from the JSON
155     * string.
156     *
157     * @param json a JSON-encoded string containing an object.
158     * @throws RuntimeException if the parse fails or doesn't yield a {@code
159     *                       JSONObject}.
160     */
161    public JSONObject(String json) {
162        this(new JSONTokener(json));
163    }
164
165    /**
166     * Creates a new {@code JSONObject} by copying mappings for the listed names
167     * from the given object. Names that aren't present in {@code copyFrom} will
168     * be skipped.
169     *
170     * @param copyFrom The source object.
171     * @param names    The names of the fields to copy.
172     * @throws RuntimeException On internal errors. Shouldn't happen.
173     */
174    public JSONObject(JSONObject copyFrom, String... names) {
175        this();
176        for (String name : names) {
177            Object value = copyFrom.opt(name);
178            if (value != null) {
179                nameValuePairs.put(name, value);
180            }
181        }
182    }
183
184
185    /**
186     * Returns a new JSONObject that is a shallow copy of this JSONObject.
187     *
188     * @since 5.4
189     */
190    public JSONObject copy()
191    {
192        JSONObject dupe = new JSONObject();
193        dupe.nameValuePairs.putAll(nameValuePairs);
194
195        return dupe;
196    }
197
198    /**
199     * Constructs a new JSONObject using a series of String keys and object values.
200     * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings
201     * (toString() will be invoked on each key).
202     *
203     * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes
204     * it much easier to initialize a JSONObject in a single statement, which is more readable.
205     *
206     * @since 5.2.0
207     */
208    public JSONObject(Object... keysAndValues)
209    {
210        this();
211
212        int i = 0;
213
214        while (i < keysAndValues.length)
215        {
216            put(keysAndValues[i++].toString(), keysAndValues[i++]);
217        }
218    }
219
220    /**
221     * Returns the number of name/value mappings in this object.
222     *
223     * @return the length of this.
224     */
225    public int length() {
226        return nameValuePairs.size();
227    }
228
229    /**
230     * Maps {@code name} to {@code value}, clobbering any existing name/value
231     * mapping with the same name. If the value is {@code null}, any existing
232     * mapping for {@code name} is removed.
233     *
234     * @param name  The name of the new value.
235     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
236     *              Integer, Long, Double, {@link #NULL}, or {@code null}. May not be
237     *              {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
238     *              infinities}.
239     * @return this object.
240     * @throws RuntimeException if the value is an invalid double (infinite or NaN).
241     */
242    public JSONObject put(String name, Object value) {
243        if (value == null) {
244            nameValuePairs.remove(name);
245            return this;
246        }
247        testValidity(value);
248        if (value instanceof Number) {
249            // deviate from the original by checking all Numbers, not just floats & doubles
250            JSON.checkDouble(((Number) value).doubleValue());
251        }
252        nameValuePairs.put(checkName(name), value);
253        return this;
254    }
255
256    /**
257     * Appends {@code value} to the array already mapped to {@code name}. If
258     * this object has no mapping for {@code name}, this inserts a new mapping.
259     * If the mapping exists but its value is not an array, the existing
260     * and new values are inserted in order into a new array which is itself
261     * mapped to {@code name}. In aggregate, this allows values to be added to a
262     * mapping one at a time.
263     *
264     * Note that {@code append(String, Object)} provides better semantics.
265     * In particular, the mapping for {@code name} will <b>always</b> be a
266     * {@link JSONArray}. Using {@code accumulate} will result in either a
267     * {@link JSONArray} or a mapping whose type is the type of {@code value}
268     * depending on the number of calls to it.
269     *
270     * @param name  The name of the field to change.
271     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
272     *              Integer, Long, Double, {@link #NULL} or null. May not be {@link
273     *              Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
274     * @return this object after mutation.
275     * @throws RuntimeException If the object being added is an invalid number.
276     */
277    // TODO: Change {@code append) to {@link #append} when append is
278    // unhidden.
279    public JSONObject accumulate(String name, Object value) {
280        Object current = nameValuePairs.get(checkName(name));
281        if (current == null) {
282            return put(name, value);
283        }
284
285        if (current instanceof JSONArray) {
286            JSONArray array = (JSONArray) current;
287            array.checkedPut(value);
288        } else {
289            JSONArray array = new JSONArray();
290            array.checkedPut(current);
291            array.checkedPut(value);
292            nameValuePairs.put(name, array);
293        }
294        return this;
295    }
296
297    /**
298     * Appends values to the array mapped to {@code name}. A new {@link JSONArray}
299     * mapping for {@code name} will be inserted if no mapping exists. If the existing
300     * mapping for {@code name} is not a {@link JSONArray}, a {@link RuntimeException}
301     * will be thrown.
302     *
303     * @param name  The name of the array to which the value should be appended.
304     * @param value The value to append.
305     * @return this object.
306     * @throws RuntimeException if {@code name} is {@code null} or if the mapping for
307     *                       {@code name} is non-null and is not a {@link JSONArray}.
308     */
309    public JSONObject append(String name, Object value) {
310        testValidity(value);
311        Object current = nameValuePairs.get(checkName(name));
312
313        final JSONArray array;
314        if (current instanceof JSONArray) {
315            array = (JSONArray) current;
316        } else if (current == null) {
317            JSONArray newArray = new JSONArray();
318            nameValuePairs.put(name, newArray);
319            array = newArray;
320        } else {
321            throw new RuntimeException("JSONObject[\"" + name + "\"] is not a JSONArray.");
322        }
323
324        array.checkedPut(value);
325
326        return this;
327    }
328
329    String checkName(String name) {
330        if (name == null) {
331            throw new RuntimeException("Names must be non-null");
332        }
333        return name;
334    }
335
336    /**
337     * Removes the named mapping if it exists; does nothing otherwise.
338     *
339     * @param name The name of the mapping to remove.
340     * @return the value previously mapped by {@code name}, or null if there was
341     * no such mapping.
342     */
343    public Object remove(String name) {
344        return nameValuePairs.remove(name);
345    }
346
347    /**
348     * Returns true if this object has no mapping for {@code name} or if it has
349     * a mapping whose value is {@link #NULL}.
350     *
351     * @param name The name of the value to check on.
352     * @return true if the field doesn't exist or is null.
353     */
354    public boolean isNull(String name) {
355        Object value = nameValuePairs.get(name);
356        return value == null || value == NULL;
357    }
358
359    /**
360     * Returns true if this object has a mapping for {@code name}. The mapping
361     * may be {@link #NULL}.
362     *
363     * @param name The name of the value to check on.
364     * @return true if this object has a field named {@code name}
365     */
366    public boolean has(String name) {
367        return nameValuePairs.containsKey(name);
368    }
369
370    /**
371     * Returns the value mapped by {@code name}, or throws if no such mapping exists.
372     *
373     * @param name The name of the value to get.
374     * @return The value.
375     * @throws RuntimeException if no such mapping exists.
376     */
377    public Object get(String name) {
378        Object result = nameValuePairs.get(name);
379        if (result == null) {
380            throw new RuntimeException("JSONObject[\"" + name + "\"] not found.");
381        }
382        return result;
383    }
384
385    /**
386     * Returns the value mapped by {@code name}, or null if no such mapping
387     * exists.
388     *
389     * @param name The name of the value to get.
390     * @return The value.
391     */
392    public Object opt(String name) {
393        return nameValuePairs.get(name);
394    }
395
396    /**
397     * Returns the value mapped by {@code name} if it exists and is a boolean or
398     * can be coerced to a boolean, or throws otherwise.
399     *
400     * @param name The name of the field we want.
401     * @return The selected value if it exists.
402     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
403     *                       to a boolean.
404     */
405    public boolean getBoolean(String name) {
406        Object object = get(name);
407        Boolean result = JSON.toBoolean(object);
408        if (result == null) {
409            throw JSON.typeMismatch(false, name, object, "Boolean");
410        }
411        return result;
412    }
413
414    /**
415     * Returns the value mapped by {@code name} if it exists and is a double or
416     * can be coerced to a double, or throws otherwise.
417     *
418     * @param name The name of the field we want.
419     * @return The selected value if it exists.
420     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
421     *                       to a double.
422     */
423    public double getDouble(String name) {
424        Object object = get(name);
425        Double result = JSON.toDouble(object);
426        if (result == null) {
427            throw JSON.typeMismatch(false, name, object, "number");
428        }
429        return result;
430    }
431
432    /**
433     * Returns the value mapped by {@code name} if it exists and is an int or
434     * can be coerced to an int, or throws otherwise.
435     *
436     * @param name The name of the field we want.
437     * @return The selected value if it exists.
438     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
439     *                       to an int.
440     */
441    public int getInt(String name) {
442        Object object = get(name);
443        Integer result = JSON.toInteger(object);
444        if (result == null) {
445            throw JSON.typeMismatch(false, name, object, "int");
446        }
447        return result;
448    }
449
450    /**
451     * Returns the value mapped by {@code name} if it exists and is a long or
452     * can be coerced to a long, or throws otherwise.
453     * Note that JSON represents numbers as doubles,
454     *
455     * so this is <a href="#lossy">lossy</a>; use strings to transfer numbers
456     * via JSON without loss.
457     *
458     * @param name The name of the field that we want.
459     * @return The value of the field.
460     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
461     *                       to a long.
462     */
463    public long getLong(String name) {
464        Object object = get(name);
465        Long result = JSON.toLong(object);
466        if (result == null) {
467            throw JSON.typeMismatch(false, name, object, "long");
468        }
469        return result;
470    }
471
472    /**
473     * Returns the value mapped by {@code name} if it exists, coercing it if
474     * necessary, or throws if no such mapping exists.
475     *
476     * @param name The name of the field we want.
477     * @return The value of the field.
478     * @throws RuntimeException if no such mapping exists.
479     */
480    public String getString(String name) {
481        Object object = get(name);
482        String result = JSON.toString(object);
483        if (result == null) {
484            throw JSON.typeMismatch(false, name, object, "String");
485        }
486        return result;
487    }
488
489    /**
490     * Returns the value mapped by {@code name} if it exists and is a {@code
491     * JSONArray}, or throws otherwise.
492     *
493     * @param name The field we want to get.
494     * @return The value of the field (if it is a JSONArray.
495     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
496     *                       JSONArray}.
497     */
498    public JSONArray getJSONArray(String name) {
499        Object object = get(name);
500        if (object instanceof JSONArray) {
501            return (JSONArray) object;
502        } else {
503            throw JSON.typeMismatch(false, name, object, "JSONArray");
504        }
505    }
506
507    /**
508     * Returns the value mapped by {@code name} if it exists and is a {@code
509     * JSONObject}, or throws otherwise.
510     *
511     * @param name The name of the field that we want.
512     * @return a specified field value (if it is a JSONObject)
513     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
514     *                       JSONObject}.
515     */
516    public JSONObject getJSONObject(String name) {
517        Object object = get(name);
518        if (object instanceof JSONObject) {
519            return (JSONObject) object;
520        } else {
521            throw JSON.typeMismatch(false, name, object, "JSONObject");
522        }
523    }
524
525    /**
526     * Returns the set of {@code String} names in this object. The returned set
527     * is a view of the keys in this object. {@link Set#remove(Object)} will remove
528     * the corresponding mapping from this object and set iterator behaviour
529     * is undefined if this object is modified after it is returned.
530     *
531     * See {@link #keys()}.
532     *
533     * @return The names in this object.
534     */
535    public Set<String> keys() {
536        return nameValuePairs.keySet();
537    }
538
539    /**
540     * Returns an array containing the string names in this object. This method
541     * returns null if this object contains no mappings.
542     *
543     * @return the names.
544     */
545    public JSONArray names() {
546        return nameValuePairs.isEmpty()
547                ? null
548                : JSONArray.from(nameValuePairs.keySet());
549    }
550
551    /**
552     * Encodes the number as a JSON string.
553     *
554     * @param number a finite value. May not be {@link Double#isNaN() NaNs} or
555     *               {@link Double#isInfinite() infinities}.
556     * @return The encoded number in string form.
557     * @throws RuntimeException On internal errors. Shouldn't happen.
558     */
559    public static String numberToString(Number number) {
560        if (number == null) {
561            throw new RuntimeException("Number must be non-null");
562        }
563
564        double doubleValue = number.doubleValue();
565        JSON.checkDouble(doubleValue);
566
567        // the original returns "-0" instead of "-0.0" for negative zero
568        if (number.equals(NEGATIVE_ZERO)) {
569            return "-0";
570        }
571
572        long longValue = number.longValue();
573        if (doubleValue == (double) longValue) {
574            return Long.toString(longValue);
575        }
576
577        return number.toString();
578    }
579
580    static String doubleToString(double d)
581    {
582        if (Double.isInfinite(d) || Double.isNaN(d))
583        {
584            return "null";
585        }
586
587        return numberToString(d);
588    }
589
590    /**
591     * Encodes {@code data} as a JSON string. This applies quotes and any
592     * necessary character escaping.
593     *
594     * @param data the string to encode. Null will be interpreted as an empty
595     *             string.
596     * @return the quoted string.
597     */
598    public static String quote(String data) {
599        if (data == null) {
600            return "\"\"";
601        }
602        try {
603            JSONStringer stringer = new JSONStringer();
604            stringer.open(JSONStringer.Scope.NULL, "");
605            stringer.string(data);
606            stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
607            return stringer.toString();
608        } catch (RuntimeException e) {
609            throw new AssertionError();
610        }
611    }
612
613
614
615    /**
616     * Prints the JSONObject using the session.
617     *
618     * @since 5.2.0
619     */
620    @Override
621    void print(JSONPrintSession session)
622    {
623        session.printSymbol('{');
624
625        session.indent();
626
627        boolean comma = false;
628
629        for (String key : keys())
630        {
631            if (comma)
632                session.printSymbol(',');
633
634            session.newline();
635
636            session.printQuoted(key);
637
638            session.printSymbol(':');
639
640            printValue(session, nameValuePairs.get(key));
641
642            comma = true;
643        }
644
645        session.outdent();
646
647        if (comma)
648            session.newline();
649
650        session.printSymbol('}');
651    }
652
653
654    /**
655     * Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using
656     * the session.
657     *
658     * @since 5.2.0
659     */
660    static void printValue(JSONPrintSession session, Object value)
661    {
662
663        if (value == null || value == NULL)
664        {
665            session.print("null");
666            return;
667        }
668        if (value instanceof JSONObject)
669        {
670            ((JSONObject) value).print(session);
671            return;
672        }
673
674        if (value instanceof JSONArray)
675        {
676            ((JSONArray) value).print(session);
677            return;
678        }
679
680        if (value instanceof JSONString)
681        {
682            String printValue = ((JSONString) value).toJSONString();
683
684            session.print(printValue);
685
686            return;
687        }
688
689        if (value instanceof Number)
690        {
691            String printValue = numberToString((Number) value);
692            session.print(printValue);
693            return;
694        }
695
696        if (value instanceof Boolean)
697        {
698            session.print(value.toString());
699
700            return;
701        }
702
703        // Otherwise it really should just be a string. Nothing else can go in.
704        session.printQuoted(value.toString());
705    }
706
707    public boolean equals(Object obj)
708    {
709        if (obj == null)
710            return false;
711
712        if (!(obj instanceof JSONObject))
713            return false;
714
715        JSONObject other = (JSONObject) obj;
716
717        return nameValuePairs.equals(other.nameValuePairs);
718    }
719
720    /**
721     * Returns a Map of the keys and values of the JSONObject. The returned map is unmodifiable.
722     * Note that changes to the JSONObject will be reflected in the map. In addition, null values in the JSONObject
723     * are represented as {@link JSONObject#NULL} in the map.
724     *
725     * @return unmodifiable map of properties and values
726     * @since 5.4
727     */
728    public Map<String, Object> toMap()
729    {
730        return Collections.unmodifiableMap(nameValuePairs);
731    }
732
733    /**
734     * Invokes {@link #put(String, Object)} for each value from the map.
735     *
736     * @param newProperties
737     *         to add to this JSONObject
738     * @return this JSONObject
739     * @since 5.4
740     */
741    public JSONObject putAll(Map<String, ?> newProperties)
742    {
743        assert newProperties != null;
744
745        for (Map.Entry<String, ?> e : newProperties.entrySet())
746        {
747            put(e.getKey(), e.getValue());
748        }
749
750        return this;
751    }
752
753
754    /**
755     * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist,
756     * or must be a JSONObject.
757     *
758     * @param key
759     * @return the nested JSONObject
760     * @throws IllegalStateException
761     *         if the current value for the key is not null and not JSONObject
762     */
763    public JSONObject in(String key)
764    {
765        assert key != null;
766
767        Object nested = nameValuePairs.get(key);
768
769        if (nested != null && !(nested instanceof JSONObject))
770        {
771            throw new IllegalStateException(String.format("JSONObject[%s] is not a JSONObject.", quote(key)));
772        }
773
774        if (nested == null)
775        {
776            nested = new JSONObject();
777            nameValuePairs.put(key, nested);
778        }
779
780        return (JSONObject) nested;
781    }
782
783    static void testValidity(Object value)
784    {
785        if (value == null)
786          throw new IllegalArgumentException("null isn't valid in JSONObject and JSONArray. Use JSONObject.NULL instead.");
787        if (value == NULL)
788        {
789            return;
790        }
791        Class<? extends Object> clazz = value.getClass();
792        if (Boolean.class.isAssignableFrom(clazz)
793            || Number.class.isAssignableFrom(clazz)
794            || String.class.isAssignableFrom(clazz)
795            || JSONArray.class.isAssignableFrom(clazz)
796            || JSONLiteral.class.isAssignableFrom(clazz)
797            || JSONObject.class.isAssignableFrom(clazz)
798            || JSONString.class.isAssignableFrom(clazz))
799        {
800            return;
801        }
802
803        throw new RuntimeException("JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type "+clazz.getName()+" is not allowed.");
804    }
805
806}