com.tinspx.util.json.JSONParserTest.java Source code

Java tutorial

Introduction

Here is the source code for com.tinspx.util.json.JSONParserTest.java

Source

/* Copyright (C) 2013-2014 Ian Teune <ian.teune@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.tinspx.util.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import static com.google.common.base.Preconditions.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.tinspx.util.collect.MapUtils;
import com.tinspx.util.io.CAWriter;
import com.tinspx.util.io.CharSequenceReader;
import static com.tinspx.util.json.HandlerEvent.*;
import com.tinspx.util.json.HandlerEvent.Validator;
import com.tinspx.util.json.utils.JSONTest;
import com.tinspx.util.json.utils.LoggingHandler;
import com.tinspx.util.json.utils.RandomJSONGenerator;
import com.tinspx.util.json.utils.TokenLoggingHandler;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.boon.json.JsonSlurper;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Testing Notes:
 * Had issue where RandomJSONGenerator was using CharSequnces as Map keys. This
 * caused lookup issues as the equality functions expect them to be Strings.
 * For future reference, Field/Map keys MUST be Strings.
 * 
 * @author Ian
 */
public class JSONParserTest {

    static final ParserOptions STRICT = ParserOptions.STRICT;
    static final ParserOptions LENIENT = ParserOptions.UNQUOTED;
    //    static final ParserOptions LENIENT = ParserOptions.LENIENT.toBuilder().allowUnquotedStrings(false).build();

    static final ParserOptions COMMAS = ParserOptions.builder().allowRedundantCommas(true).build();
    static final ParserOptions COMMENTS = ParserOptions.builder().allowComments(true).build();
    static final ParserOptions YAML = ParserOptions.builder().allowYAMLComments(true).build();
    static final ParserOptions ALL_COMMENTS = ParserOptions.builder().allowComments(true).allowYAMLComments(true)
            .build();
    static final ParserOptions UNQUOTED_FIELDS = ParserOptions.builder().allowUnquotedFieldNames(true).build();
    static final ParserOptions UNQUOTED_STRINGS = ParserOptions.builder().allowUnquotedStrings(true).build();
    static final ParserOptions SINGLE = ParserOptions.builder().allowSingleQuotes(true).build();
    static final ParserOptions UNESCAPED_CONTROL = ParserOptions.builder().allowUnescapedControlChars(true).build();
    static final ParserOptions CASE = ParserOptions.builder().allowCaseInsensitivePrimitives(true).build();
    static final ParserOptions BACKSLASH = ParserOptions.builder().allowBackslashEscapingAnyChar(true).build();
    static final ParserOptions SINGLE_COMMAS = SINGLE.or(COMMAS);

    RandomJSONGenerator gen;
    //    Object json;

    public JSONParserTest() {
        gen = RandomJSONGenerator.builder().maxFieldLength(16).maxStringLength(32).maxObjectLength(8)
                .maxArrayLength(16).maxBigIntegerLength(48).build();
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }

    static final String FILE = "C:\\Users\\Ian\\Documents\\test\\test.txt";

    static final String RANDOM = "C:\\Users\\Ian\\Documents\\test\\random.txt";
    static final String PARSEED = "C:\\Users\\Ian\\Documents\\test\\parsed.txt";

    static final String LOG_HANDLER = "C:\\Users\\Ian\\Documents\\test\\log_handler.txt";
    static final String LOG_JSON = "C:\\Users\\Ian\\Documents\\test\\log_json.txt";

    static final String FORMATTED = "C:\\Users\\Ian\\Documents\\test\\formatted.txt";
    static final String RAW = "C:\\Users\\Ian\\Documents\\test\\raw.txt";

    @Test
    public void testRandomStandardParsing() throws IOException {
        CAWriter w = new CAWriter(1024 * 1024 * 16);
        for (int i = 0; i < 6; i++) {
            testRandomStandardParsing(w);
        }
    }

    @SuppressWarnings({ "CallToPrintStackTrace", "BroadCatchBlock", "TooBroadCatch" })
    private void testRandomStandardParsing(CAWriter w) throws IOException {
        Object value = gen.buildRandomJSON(16, 12);

        w.reset();
        ConfigurableWriter.INSTANCE.writeTo(w, value);
        Object parse = null;
        try {
            parse = JSONValue.parse(w);
        } catch (Exception ex) {
            ex.printStackTrace();
            Assert.fail("parse exception normal");
        }
        assertJsonEquals(value, parse, "normal");

        w.reset();
        IndentingWriter.indentingBuilder().build().writeTo(w, value);
        parse = null;
        try {
            parse = JSONValue.parse(new CharSequenceReader(w));
        } catch (Exception ex) {
            ex.printStackTrace();
            Assert.fail("parse exception indenting");
        }
        assertJsonEquals(value, parse, "indenting");

        w.reset();
        JSONValue.writeTo(w, value);
        parse = null;
        try {
            parse = JSONValue.parse((Readable) CharBuffer.wrap(w.toCharArray()));
        } catch (Exception ex) {
            ex.printStackTrace();
            Assert.fail("parse exception indenting");
        }
        assertJsonEquals(value, parse, "indenting");
    }

    static void writeFormatted(String file, Object value) throws IOException {
        Writer caw = new BufferedWriter(new FileWriter(file), 1024 * 16);
        //        caw.write("start json\n");
        IndentingWriter.indentingBuilder().build().writeTo(caw, value);
        //        caw.write("\nend of json\n");
        caw.flush();
        caw.close();
    }

    //    @Test
    public void printRandom() throws IOException {
        RandomJSONGenerator g = RandomJSONGenerator.builder().maxFieldLength(64).maxStringLength(256)
                .maxObjectLength(16).maxArrayLength(32).bigNumbers(false).build();

        Object value = g.buildRandomJSON(2, 4);

        Writer w = writer_utf(RAW);
        ConfigurableWriter.INSTANCE.writeTo(w, value);
        w.flush();
        w.close();

        w = writer_utf(FORMATTED);
        IndentingWriter.indentingBuilder().build().writeTo(w, value);
        w.flush();
        w.close();

        assertTrue(true);
    }

    static final String SAMPLE_FILE = "C:\\Users\\Ian\\Documents\\test\\json_sample.txt";

    /**
     * tests boon, JSONSimple, and tin on the json sample file from json.org,
     * All parsers pass and the parsed objects are all equal to each other
     */
    //    @Test
    public void testSampleJSON() throws IOException {
        String sample = Files.toString(new File(SAMPLE_FILE), Charsets.UTF_8);

        Object simple = org.json.simple.JSONValue.parse(sample);
        Object tin = JSONValue.parse(sample);
        Object boon = new JsonSlurper().parseText(sample);

        assertJsonEquals(tin, simple, "tin - simple");
        assertJsonEquals(tin, boon, "tin - boon");
        assertJsonEquals(simple, boon, "simple - boon");
    }

    /**
     * Tests JSONSimple, boon, and tin on a random JSON object
     * boon fails this test most of the time, SimpleJSON will fail if large
     * number are enabled
     */
    //    @Test
    @SuppressWarnings("UnusedAssignment")
    public void testOtherJSONParsers() throws IOException {
        Object value = gen.buildRandomJSON(14, 13);

        CAWriter w = new CAWriter(1024 * 1024);
        ConfigurableWriter.INSTANCE.writeTo(w, value);
        String input = w.toString();
        w = null;

        Object tin = JSONValue.parse(input);
        assertJsonEquals(value, tin, "tin");
        tin = null;

        Object simple = org.json.simple.JSONValue.parse(input);
        assertJsonEquals(value, simple, "simple");
        simple = null;

        //boon fails, haha - so much for the fastes json parser in the world
        Object boon = new JsonSlurper().parseText(input);
        assertJsonEquals(value, boon, "boon");
    }

    /**
     * tests deeply nsted lists on other parsers. only json-simple can handle
     * it - boon and jackson throw StackOverflow
     */
    //    @Test
    public void testNestedListOtherParsers() throws IOException {
        Object value = buildNestedList(1024 * 300);
        String json = JSONValue.toString(value);

        assertJsonEqualsNoRecurse(value, org.json.simple.JSONValue.parse(json), "simple");
        assertJsonEqualsNoRecurse(value, new JsonSlurper().parseText(json), "boon");

        //can't etst equality, but see if it can parse
        new ObjectMapper().readTree(json);
    }

    @Test
    public void testDeeplyNestedList() throws IOException {
        Object value = buildNestedList(1024 * 300);
        assertJsonEqualsNoRecurse(value, JSONValue.parse(JSONValue.toString(value)));
    }

    @Test
    public void testDeeplyNestedMap() throws IOException {
        Object value = buildNestedMap(1024 * 300, "k");
        assertJsonEqualsNoRecurse(value, JSONValue.parse(JSONValue.toString(value)));
    }

    public static List<Object> buildNestedList(int depth) {
        final List<Object> head = Lists.newArrayListWithCapacity(1);
        List<Object> tail = head;
        for (; depth > 0; depth--) {
            List<Object> list = Lists.newArrayListWithCapacity(1);
            tail.add(list);
            tail = list;
        }
        return head;
    }

    public static Object buildNestedMap(int depth, String key) {
        checkNotNull(key);
        final Map<String, Object> head = MapUtils.newMutableSingletonMap();
        Map<String, Object> tail = head;
        for (; depth > 0; depth--) {
            Map<String, Object> map = MapUtils.newMutableSingletonMap();
            tail.put(key, map);
            tail = map;
        }
        return head;
    }

    static void assertJsonEqualsNoRecurse(Object a, Object b) {
        assertJsonEqualsNoRecurse(a, b, "json not equal");
    }

    static void assertJsonEqualsNoRecurse(Object a, Object b, String msg) {
        assertTrue(msg, JSONTest.jsonEqualsNoRecurse(a, b));
        assertTrue(msg, JSONTest.jsonEqualsNoRecurse(b, a));
    }

    static void assertJsonEquals(Object a, Object b) {
        assertJsonEquals(a, b, "json not equal");
    }

    static void assertJsonEquals(Object a, Object b, String msg) {
        JSONTest.jsonEqualsThrow(a, b);
        JSONTest.jsonEqualsThrow(b, a);
        assertTrue(msg, JSONTest.jsonEquals(a, b));
        assertTrue(msg, JSONTest.jsonEquals(b, a));
        assertTrue(msg, JSONTest.jsonEqualsNoRecurse(a, b));
        assertTrue(msg, JSONTest.jsonEqualsNoRecurse(b, a));
    }

    static Writer writer(String file) throws IOException {
        return new BufferedWriter(new FileWriter(file), 1024 * 32);
    }

    static Writer writer_utf(String file) throws IOException {
        return new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(file), 1024 * 64),
                Charsets.UTF_8);
    }

    //    @Test
    public void logParse() throws IOException {
        Object value = gen.buildRandomJSON(16, 12);
        writeFormatted(LOG_JSON, value);

        CAWriter w = new CAWriter(1024 * 64);
        ConfigurableWriter.INSTANCE.writeTo(w, value);

        Writer out = writer(LOG_HANDLER);
        JSONParser.create(stream(w), new LoggingHandler(out), STRICT).parseFully();
        out.flush();
        out.close();
        assertTrue(true);
    }

    //    @Test
    public void testTokenLog() throws IOException {
        Object value = gen.buildRandomJSON(16, 12);
        //        writeFormatted(LOG_JSON, value);

        CAWriter w = new CAWriter(1024 * 64);
        ConfigurableWriter.INSTANCE.writeTo(w, value);

        CAWriter tokens = new CAWriter(1024 * 64);
        JSONParser.create(stream(w), new TokenLoggingHandler(tokens), STRICT).parseFully();

        //        Writer out = writer(LOG_HANDLER);
        //        out.append(tokens);
        //        out.flush();
        //        out.close();

        TokenLoggingHandler.verifyTokens(tokens);
        assertTrue(true);
    }

    //    @Test
    public void testFrag1() throws IOException {
        String json = "], 'string', ]".replace('\'', '"');
        Validator<JSONParser> handler = validator(SJ, SA, EA, SA);

        JSONParser.create(stream(json), handler).parseFully();

        handler.finish();
    }

    static void writeIndentingOut(Object value) throws IOException {
        Writer w = new OutputStreamWriter(System.out);
        IndentingWriter.create().writeTo(w, value);
        w.flush();
        w.close();
        System.out.println();
    }

    //    @Test
    public void printEvents() throws IOException {
        //        String json = "], 'string']".replace('\'', '"');
        //        String json = ",\"89\":null}], 'string']".replace('\'', '"');
        String json = "'key': 'value' ,\n'89':null}], 'string'], 'another string', 124.56, [{}]], [], 'test', {},"
                .replace('\'', '"');

        Writer w = new OutputStreamWriter(System.out);
        JSONParser.create(stream(json), new LoggingHandler(w)).parseFragment();
        w.flush();
        System.out.println();

        FragmentBuilder frag = new FragmentBuilder();
        JSONParser.create(stream(json), frag).parseFragment();
        System.out.println(frag.get());

        writeIndentingOut(frag.get());

        assertTrue(true);
    }

    static JSONStream stream(CharSequence seq) {
        return new CharSequenceStream(seq);
    }

    @Test
    public void basicTest1() throws IOException {
        String json = "[true, false, null, [], {}, {\"key\":\"value\"}, \"string\", 1]";
        Validator<JSONParser> handler = validator(SJ, SA, p(true), p(false), p(null), SA, EA, SO, EO, SO, sf("key"),
                p("value"), EF, EO, p("string"), p(1), EA, EJ);

        JSONParser.create(stream(json), handler).parseFully();
        handler.finish();
    }

    @Test
    public void basicTest2() throws IOException {
        String json = " [true,\n\r \tfalse, null,[], {}, {\"key\":\"value\", \"array\" :\t[[], 8]}, \"string\", 1]\n\r";
        Validator<JSONParser> handler = validator(SJ, SA, p(true), p(false), p(null), SA, EA, SO, EO, SO, sf("key"),
                p("value"), EF, sf("array"), SA, SA, EA, p(8), EA, EF, EO, p("string"), p(1), EA, EJ);

        JSONParser.create(stream(json), handler).parseFully();
        handler.finish();
    }

    @Test
    public void basicTest3() throws IOException {
        String json = " [true,\n\r \tfalse, null,[], {}, {\"ke\u3763y\":\"value\", \"array\uD834\uDD1E\" :\t[[], 8]}, \"string\", 1]\n\r";
        Validator<JSONParser> handler = validator(SJ, SA, p(true), p(false), p(null), SA, EA, SO, EO, SO,
                sf("ke\u3763y"), p("value"), EF, sf("array\uD834\uDD1E"), SA, SA, EA, p(8), EA, EF, EO, p("string"),
                p(1), EA, EJ);

        JSONParser.create(stream(json), handler).parseFully();
        handler.finish();
    }

    @Test
    public void testParseLiteral() throws IOException {
        assertEquals("test string", JSONValue.parse("\"test string\""));
        assertEquals(true, JSONValue.parse("true"));
        assertEquals(false, JSONValue.parse("false"));
        assertEquals(null, JSONValue.parse("null"));
        assertEquals(89, JSONValue.parse("89"));
        assertEquals(new BigDecimal("89.789"), JSONValue.parse("89.789"));
    }

    static void fpass(Object value, String json) throws IOException {
        fpass(value, json, STRICT);
    }

    static void fpass(Object value, String json, ParserOptions... options) throws IOException {
        assertFalse(value instanceof ParserOptions);
        assertTrue(json instanceof String);
        int count = 0;
        for (ParserOptions op : options) {
            count++;
            assertTrue(op instanceof ParserOptions);
            assertJsonEquals(value, doFullParseFragment(new CharSequenceStream(json), op));
            assertJsonEquals(value, doFullParseFragment(new ReaderStream(new CharSequenceReader(json)), op));
        }
        assertTrue(count > 0);
    }

    static void pass(Object value, String json, ParserOptions... options) throws IOException {
        assertFalse(value instanceof ParserOptions);
        assertTrue(json instanceof String);
        int count = 0;
        for (ParserOptions op : options) {
            count++;
            assertTrue(op instanceof ParserOptions);
            assertJsonEquals(value, doFullParse(new CharSequenceStream(json), op));
            assertJsonEquals(value, doFullParse(new ReaderStream(new CharSequenceReader(json)), op));
        }
        assertTrue(count > 0);
    }

    static Object doFullParse(JSONStream source, ParserOptions options) throws IOException {
        ObjectBuilder builder = new ObjectBuilder();
        JSONParser parser = JSONParser.create(source, builder, options);
        parser.parseFully();
        assertTrue(parser.isComplete());
        assertTrue(builder.isComplete());
        return builder.get();
    }

    static Object doFullParseFragment(JSONStream source, ParserOptions options) throws IOException {
        FragmentBuilder builder = new FragmentBuilder();
        JSONParser parser = JSONParser.create(source, builder, options);
        parser.parseFragment();
        //can't assert complete because fragment tests are not always complete
        //        assertTrue(parser.isComplete());
        //        assertTrue(builder.isComplete());
        return builder.get();
    }

    static void pass(String json) throws IOException {
        pass(json, STRICT);
    }

    static void pass(String json, ParserOptions... options) throws IOException {
        assertTrue(json instanceof String);
        int count = 0;
        for (ParserOptions op : options) {
            count++;
            assertTrue(op instanceof ParserOptions);
            doFullParse(new CharSequenceStream(json), op);
            doFullParse(new ReaderStream(new CharSequenceReader(json)), op);
        }
        assertTrue(count > 0);
    }

    static void fail(String json) throws IOException {
        fail(json, STRICT);
    }

    static void fail(String json, ParserOptions... options) throws IOException {
        assertTrue(json instanceof String);
        int count = 0;
        for (ParserOptions op : options) {
            count++;
            assertTrue(op instanceof ParserOptions);
            try {
                JSONValue.parse(json, op);
                Assert.fail(json + "\n (CharSequence) did not fail with options: " + op);
            } catch (JSONException ex) {
            }
            try {
                JSONValue.parse(new CharSequenceReader(json), op);
                Assert.fail(json + "\n (Readable) did not fail with options: " + op);
            } catch (JSONException ex) {
            }
        }
        assertTrue(count > 0);
    }

    static void ffail(String json) throws IOException {
        ffail(json, STRICT);
    }

    static void ffail(String json, ParserOptions... options) throws IOException {
        assertTrue(json instanceof String);
        int count = 0;
        for (ParserOptions op : options) {
            count++;
            assertTrue(op instanceof ParserOptions);
            try {
                JSONValue.parseFragment(json, op);
                Assert.fail(json + "\n (CharSequence) did not fail with options: " + op);
            } catch (JSONException ex) {
            }
            try {
                JSONValue.parseFragment(new CharSequenceReader(json), op);
                Assert.fail(json + "\n (Readable) did not fail with options: " + op);
            } catch (JSONException ex) {
            }
        }
        assertTrue(count > 0);
    }

    @Test
    public void testCommas() throws IOException {
        //array commas
        pass("[]");
        pass("[true]");
        pass("[false,null,100, \"test\"]");

        //tests of primitives
        fail("[,]");
        fail("[,,,,,]");
        fail("[,true]");
        fail("[,,,,,false]");
        fail("[false,]");
        fail("[null,,,,,]");
        fail("[,8,\"\"]");
        fail("[,,,,8,\"\"]");
        fail("[8,,\"\"]");
        fail("[100,,,,,\"test\"]");
        fail("[,,,,,,null,,,,,\"test\",,,,,,]");
        pass("[,]", COMMAS);
        pass("[,,,,,]", COMMAS);
        pass("[,true]", COMMAS);
        pass("[,,,,,false]", COMMAS);
        pass("[false,]", COMMAS);
        pass("[null,,,,,]", COMMAS);
        pass("[,8,\"\"]", COMMAS);
        pass("[,,,,8,\"\"]", COMMAS);
        pass("[8,,\"\"]", COMMAS);
        pass("[100,,,,,\"test\"]", COMMAS);
        pass("[,,,,,,null,,,,,\"test\",,,,,,]", COMMAS);

        //now with objects/arrays
        fail("[,[]]");
        fail("[,{}]");
        fail("[,,,,,[]]");
        fail("[,,,,,{}]");
        fail("[[],]");
        fail("[{},]");
        fail("[[],,,,,]");
        fail("[{},,,,,]");
        fail("[,[],{}]");
        fail("[,,,,{},[]]");
        fail("[[],,{}]");
        fail("[[],,,,,{}]");
        fail("[,,,,,,{},,,,,{},,,,,,]");
        pass("[,[]]", COMMAS);
        pass("[,{}]", COMMAS);
        pass("[,,,,,[]]", COMMAS);
        pass("[,,,,,{}]", COMMAS);
        pass("[[],]", COMMAS);
        pass("[{},]", COMMAS);
        pass("[[],,,,,]", COMMAS);
        pass("[{},,,,,]", COMMAS);
        pass("[,[],{}]", COMMAS);
        pass("[,,,,{},[]]", COMMAS);
        pass("[[],,{}]", COMMAS);
        pass("[[],,,,,{}]", COMMAS);
        pass("[,,,,,,{},,,,,{},,,,,,]", COMMAS);

        //now nested
        fail("[,,,,[,null,100,[,,,]],,[,false,,{},,],,[false,[,]],,,[true,]]");
        pass("[,,,,[,null,100,[,,,]],,[,false,,{},,],,[false,[,]],,,[true,]]", COMMAS);

        //object commas
        pass("{}");
        pass("{'k' : false}".replace('\'', '"'));
        pass("{'k' : false, 'key' : 'test'}".replace('\'', '"'));

        fail("{,}".replace('\'', '"'));
        fail("{,,,,,}".replace('\'', '"'));
        fail("{, 'k':false}".replace('\'', '"'));
        fail("{, ,,,,,\n,'k':false}".replace('\'', '"'));
        fail("{'k':false,}".replace('\'', '"'));
        fail("{'k':false,,,\t, , }".replace('\'', '"'));
        fail("{'k':false,,'a':'b'}".replace('\'', '"'));
        fail("{'k':false,, \t,,,'a':null}".replace('\'', '"'));
        fail("{,'k':100,'a':null}".replace('\'', '"'));
        fail("{\n,,,\t,'k':100,'a':null}".replace('\'', '"'));
        fail("{'k':100,'a':true,}".replace('\'', '"'));
        fail("{'k':100,'a':true,,,\n\tr\\r, }".replace('\'', '"'));
        fail("{,,,,'k':100,,\n,,'a':true,,,\n\t\r, }".replace('\'', '"'));
        pass("{,}".replace('\'', '"'), COMMAS);
        pass("{,,,,,}".replace('\'', '"'), COMMAS);
        pass("{, 'k':false}".replace('\'', '"'), COMMAS);
        pass("{, ,,,,,\n,'k':false}".replace('\'', '"'), COMMAS);
        pass("{'k':false,}".replace('\'', '"'), COMMAS);
        pass("{'k':false,,,\t, , }".replace('\'', '"'), COMMAS);
        pass("{'k':false,,'a':'b'}".replace('\'', '"'), COMMAS);
        pass("{'k':false,, \t,,,'a':null}".replace('\'', '"'), COMMAS);
        pass("{,'k':100,'a':null}".replace('\'', '"'), COMMAS);
        pass("{\n,,,\t,'k':100,'a':null}".replace('\'', '"'), COMMAS);
        pass("{'k':100,'a':true,}".replace('\'', '"'), COMMAS);
        pass("{'k':100,'a':true,,,\n\t\r, }".replace('\'', '"'), COMMAS);
        pass("{,,,,'k':100,,\n,,'a':true,,,\n\t\r, }".replace('\'', '"'), COMMAS);

        //nested objects and arrays
        fail("{,,'k' : {, 'test' : [,,,,null,['valid;'],],,,,'key2': 544,,,},,\n\r,, 't':[[,,],\t[null,,],false,{'valid':9}], 'h': [,,false],,\n, }"
                .replace('\'', '"'));
        pass("{,,'k' : {, 'test' : [,,,,null,['valid;'],],,,,'key2': 544,,,},,\n\r,, 't':[[,,],\t[null,,],false,{'valid':9}], 'h': [,,false],,\n, }"
                .replace('\'', '"'), COMMAS);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testComments() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        final ParserOptions CLENIENT = LENIENT.toBuilder().allowUnquotedStrings(false).build();

        Object value = Arrays.asList(false, null, 100);

        //c style comments
        fail("[false //single line invalid[11[,false{json\"\u0000\t\n//\"\u0000\r,null/* mulitline comment \"\r\n\t \u0001 ** // **/ ,100]",
                STRICT, YAML);
        pass(value,
                "[false //single line invalid[11[,false{json\"\u0000\t\n//\"\u0000\r,null/* mulitline comment \"\r\n\t \u0001 ** // **/ ,100]",
                COMMENTS, CLENIENT);

        //YAML comments
        fail("[false #single line invalid[11[,false{json\"\u0000\t\n#\"\u0000\r,null ,100]", STRICT, COMMENTS);
        pass(value, "[false #single line invalid[11[,false{json\"\u0000\t\n#\"\u0000\r,null ,100]", CLENIENT, YAML);

        //both styles
        fail("[false //single line invalid[11[,false{json\"\u0000\t\n#\"\u0000\r,null/* mulitline comment \"\r\n\t \u0001 ** // **/ ,100]",
                STRICT, YAML, COMMENTS);
        pass(value,
                "[false //single line invalid[11[,false{json\"\u0000\t\n#\"\u0000\r,null/* mulitline comment \"\r\n\t \u0001 ** // **/ ,100]",
                ALL_COMMENTS, CLENIENT);
    }

    @Test
    public void testUnquotedFieldNames() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        Map<String, Object> map = Maps.newHashMap();
        map.put("field", false);
        map.put("0key", null);
        map.put("s'tr\r\t\n\u0000 ", "string");

        fail("{ field : false, 0key : null, s'tr\\r\\t\\n\\u0000\\u0020 : \"string\"}");
        pass(map, "{ field : false, 0key : null, s'tr\\r\\t\\n\\u0000\\u0020 : \"string\"}", UNQUOTED_FIELDS,
                LENIENT);

        //no field text at all
        map.put("", 789);
        fail("{ :\"over\", field : false, 0key : null, s'tr\\r\\t\\n\\u0000\\u0020 : \"string\", : 789}");
        pass(map, "{ :\"over\", field : false, 0key : null, s'tr\\r\\t\\n\\u0000\\u0020 : \"string\", : 789}",
                UNQUOTED_FIELDS, LENIENT);
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private static Map map(Object... contents) {
        assertTrue(contents.length % 2 == 0);
        Map map = new HashMap();
        for (int i = 0; i < contents.length; i += 2) {
            assertFalse(map.containsKey(contents[i]));
            map.put(contents[i], contents[i + 1]);
        }
        return map;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private static List list(Object... contents) {
        return Arrays.asList(contents);
    }

    @Test
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void testUnquotedStrings() throws IOException {
        //map tests
        fail("{'a': nulL}".replace('\'', '"'));
        pass(map("a", "nulL"), "{'a': nulL}".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("{'a': 23a}".replace('\'', '"'));
        pass(map("a", "23a"), "{'a': 23a}".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("{'a': true2}".replace('\'', '"'));
        pass(map("a", "true2"), "{'a': true2}".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("{'a': ap[{}".replace('\'', '"'));
        pass(map("a", "ap[{"), "{'a': ap[{}".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("{'a': }".replace('\'', '"'));
        pass(map("a", ""), "{'a': }".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("{'a': ,}".replace('\'', '"'), STRICT, UNQUOTED_STRINGS);
        pass(map("a", ""), "{'a': ,}".replace('\'', '"'), UNQUOTED_STRINGS.or(COMMAS));

        fail("{'a': ,'b' :}".replace('\'', '"'));
        pass(map("a", "", "b", ""), "{'a': ,'b' :}".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("{'a': ap[]{,'b' :}".replace('\'', '"'));
        pass(map("a", "ap[]{", "b", ""), "{'a': ap[]{,'b' :}".replace('\'', '"'), UNQUOTED_STRINGS);

        //list tests
        fail("[nulL]".replace('\'', '"'));
        pass(list("nulL"), "[nulL]".replace('\'', '"'), UNQUOTED_STRINGS);

        fail("[true2, 23a, false{}[: , true2\n, ap[{} ]".replace('\'', '"'));
        pass(list("true2", "23a", "false{}[:", "true2", "ap[{}"),
                "[true2, 23a, false{}[: , true2\n, ap[{} ]".replace('\'', '"'), UNQUOTED_STRINGS);
    }

    @Test
    public void testSingleQuotes() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        List<Object> value = Lists.newArrayList();
        Map<String, Object> map = Maps.newHashMap();
        map.put("single", "single");
        map.put("s", "d");
        map.put("d", "test");

        value.add("single quote");
        value.add("double");
        value.add(map);

        fail("['single quote', \"double\", {'single' : 'single', 's' : \"d\", \"d\" : 'test'}]");
        pass(value, "['single quote', \"double\", {'single' : 'single', 's' : \"d\", \"d\" : 'test'}]", SINGLE,
                LENIENT);
    }

    @Test
    public void testUnescapedControlChars() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        List<Object> value = Lists.newArrayList();
        Map<String, Object> map = Maps.newHashMap();
        map.put("\r\t\b\n\f\u0003", "\u0005\u001F");
        value.add("\u0000\r\t\n\t\b\f\u0012\u000b");
        value.add(map);

        fail("['\u0000\r\t\n\t\b\f\u0012\\u000b', {'\r\t\b\n\f\u0003' : '\u0005\u001F'}]".replace('\'', '"'));
        pass(value, "['\u0000\r\t\n\t\b\f\u0012\\u000b', {'\r\t\b\n\f\u0003' : '\u0005\u001F'}]".replace('\'', '"'),
                UNESCAPED_CONTROL, LENIENT);
        pass(value, "['\u0000\r\t\n\t\b\f\u0012\\u000b', {'\r\t\b\n\f\u0003' : '\u0005\u001F'}]",
                SINGLE.or(UNESCAPED_CONTROL), LENIENT);
    }

    @Test
    public void testCaseInsensitiveLiterals() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        List<Object> value = Arrays.<Object>asList(true, true, true, false, false, false, null, null, null);

        fail("[True, trUe, TRUE, False, falSe, FALSE, Null, nuLl, NULL]");
        pass(value, "[True, trUe, TRUE, False, falSe, FALSE, Null, nuLl, NULL]", CASE, LENIENT);
    }

    @Test
    public void testBackslashAny() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        Map<String, Object> map = Maps.newHashMap();
        map.put("\test}", " \" \t");
        List<Object> value = Arrays.<Object>asList("5o", map);

        fail("[\"\\5\\o\", {\"\\test\\}\" : \" \\\" \\\t\"}]");
        pass(value, "[\"\\5\\o\", {\"\\test\\}\" : \" \\\" \\\t\"}]", BACKSLASH, LENIENT);
    }

    private static double d(String str) {
        return Double.parseDouble(str);
    }

    private static BigDecimal bd(String str) {
        return new BigDecimal(str);
    }

    private static BigInteger bi(String str, int radix) {
        return new BigInteger(str, radix);
    }

    @Test
    public void testAllNonStandardFeatures() throws IOException {
        assertEquals(ALL_COMMENTS, COMMENTS.or(YAML));
        Map<String, Object> map = Maps.newHashMap();
        map.put("unq'8\u0000", "test");

        List<Object> value = Arrays.<Object>asList(true, false, null, 0xFFF, 0xFFFFFFFFFL,
                bi("FFFFFFFFFFFFFFFFFFFFFFF", 16), d("0xabcd.45p8d"), d("89f"), "\u0000\ny", "\t7", Double.NaN,
                Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 8, 0, 0, d("0x00p5"), bd(".89"),
                d("+.9D"), map);

        fail("[,True, False, Null,, 0xFFF, 0xFFFFFFFFF, 0xFFFFFFFFFFFFFFFFFFFFFFF, /* 100[ */ 0xabcd.45p8d, 89f, # y5{\n \"\u0000\n\\y\", '\t\\7', NaN, nan, Inf, -inFiNitY , 0008, +0, //90{'90\r -0, 0x00p5, .89, +.9D, { , unq'8\u0000\n : 'test' ,,} ]");
        pass(value,
                "[,True, False, Null,, 0xFFF, 0xFFFFFFFFF, 0xFFFFFFFFFFFFFFFFFFFFFFF, /* 100[ */ 0xabcd.45p8d, 89f, # y5{\n \"\u0000\n\\y\", '\t\\7', NaN, nan, Inf, -inFiNitY , 0008, +0, //90{'90\r -0, 0x00p5, .89, +.9D, { , unq'8\u0000\n : 'test' ,,} ]",
                LENIENT);
    }

    @Test
    public void testParseFirstObject() throws IOException {
        Map<String, Object> map = Maps.newHashMap();
        map.put("k", "test");
        map.put("n", 99);

        String json = "[ null, {\"k\" : \"test\", \"n\" : 99} ] invali json [";

        ObjectBuilder builder = new ObjectBuilder();
        JSONParser parser = JSONParser.create(new CharSequenceStream(json), builder, STRICT);
        parser.parseFirstObject();

        assertTrue(parser.isComplete());
        assertTrue(builder.isComplete());
        assertJsonEquals(map, builder.get());
    }

    @Test
    public void testParseFirstArray() throws IOException {
        String json = " {}}}}}}} [null, true, false, 1000] #67[";

        ObjectBuilder builder = new ObjectBuilder();
        JSONParser parser = JSONParser.create(new CharSequenceStream(json), builder, STRICT);
        parser.parseFirstArray();

        assertTrue(parser.isComplete());
        assertTrue(builder.isComplete());
        assertJsonEquals(Arrays.<Object>asList(null, true, false, 1000), builder.get());
    }

    @Test
    public void testParseFirst() throws IOException {
        String json = "[null, true, false, 1000] #67[";

        ObjectBuilder builder = new ObjectBuilder();
        JSONParser parser = JSONParser.create(new CharSequenceStream(json), builder, STRICT);
        parser.parseFirst();

        assertTrue(parser.isComplete());
        assertTrue(builder.isComplete());
        assertJsonEquals(Arrays.<Object>asList(null, true, false, 1000), builder.get());

        Map<String, Object> map = Maps.newHashMap();
        map.put("k", "test");
        map.put("n", 99);

        json = "{\"k\" : \"test\", \"n\" : 99} ]5t6$ invali json [";

        builder = new ObjectBuilder();
        parser = JSONParser.create(new CharSequenceStream(json), builder, STRICT);
        parser.parseFirst();

        assertTrue(parser.isComplete());
        assertTrue(builder.isComplete());
        assertJsonEquals(map, builder.get());
    }

    @Test
    @SuppressWarnings("StringEquality")
    public void testParsePart() throws IOException {
        //        String text = " [true,\n\r \tfalse, null,[], {}, {\"key\":\"value\", \"array\" :\t[[], 8]}, \"string\", 1]\n\r";
        String json[] = { " [true,\n\r \tfalse, null,[], {}, ", "{\"key\":\"value\", \"array\" ",
                ":\t[[], 8]}, \"string\", 1", "]", "\n\r \t" };

        Map<String, Object> map = Maps.newHashMap();
        map.put("key", "value");
        map.put("array", Arrays.<Object>asList(Collections.emptyList(), 8));
        List<Object> value = Arrays.<Object>asList(true, false, null, Collections.emptyList(),
                Collections.emptyMap(), map, "string", 1);

        Validator<JSONParser> validator = validator(SJ, SA, p(true), p(false), p(null), SA, EA, SO, EO, SO,
                sf("key"), p("value"), EF, sf("array"), SA, SA, EA, p(8), EA, EF, EO, p("string"), p(1), EA, EJ);

        JSONParser parser = JSONParser.create(Streams.empty(), validator, STRICT);

        for (String part : json) {
            parser.setSource(part);
            parser.parsePart();
            if (part != json[3] && part != json[4]) {
                assertFalse(parser.isComplete());
            }
        }
        parser.finish();
        assertTrue(parser.isComplete());
        validator.finish();

        //now use the builder
        ObjectBuilder builder = new ObjectBuilder();
        parser.reset();
        parser.setHandler(builder);
        for (String part : json) {
            parser.setSource(part);
            parser.parsePart();
            if (part != json[3] && part != json[4]) {
                assertFalse(parser.isComplete());
                assertFalse(builder.isComplete());
            }
        }
        parser.finish();
        assertTrue(parser.isComplete());
        assertTrue(builder.isComplete());
        assertJsonEquals(value, builder.get());
    }

    static void assertComplete(JSONParser parser) {
        assertTrue(parser.isComplete());
        JSONHandler<?> handler = parser.getHandler();
        if (handler instanceof ObjectBuilder) {
            assertTrue(((ObjectBuilder) handler).isComplete());
        } else if (handler instanceof FragmentBuilder) {
            assertTrue(((FragmentBuilder) handler).isComplete());
        }
    }

    static void assertNotComplete(JSONParser parser) {
        assertFalse(parser.isComplete());
        JSONHandler<?> handler = parser.getHandler();
        if (handler instanceof ObjectBuilder) {
            assertFalse(((ObjectBuilder) handler).isComplete());
        } else if (handler instanceof FragmentBuilder) {
            assertFalse(((FragmentBuilder) handler).isComplete());
        }
    }

    @Test
    public void testParseFragmentMap() throws IOException {
        Map<String, Object> map = Maps.newHashMap();

        fpass(map, ": false");
        fpass(map, ": false}");
        fpass(map, ": {}");
        fpass(map, ": []}");
        fpass(map, ": [false]");
        fpass(map, ": [false, [true, [{}]], {}]}");
        fpass(map, ": [false, [true, [{}]], {}]");
        fpass(map, ": false}");
        fpass(map, ": 'testing'}".replace('\'', '"'));
        fpass(map, ": 'testing'}, \n".replace('\'', '"'));
        fpass(map, "}");
        fpass(map, "\n} ,");
        fpass(map, ", false} ,");
        fpass(map, ",'only one root'} ,".replace('\'', '"'));
        fpass(map, ", [false, [true, [{}, [false]]]]} ,");
        fpass(map, "false}");
        fpass(map, ", []} ,\n");
        fpass(map, " [false, [true, [{}]], {}]}");
        fpass(map, " false}");

        map.put("k", false);
        fpass(map, " 'k' : false", SINGLE);
        fpass(map, " 'k' : false}\t", SINGLE);
        fpass(map, " ,'k' : false", SINGLE);
        fpass(map, " ,'k' : false}", SINGLE);
        fpass(map, "false ,'k' : false}", SINGLE);
        fpass(map, ", 'testing' ,'k' : false}", SINGLE);
        fpass(map, "false ,'k' : false", SINGLE);
        fpass(map, "9959727.8713357 ,'k' : false", SINGLE);
        fpass(map, ", [[[[{}, false, []]]], {}] ,'k' : false", SINGLE);
        fpass(map, ", [[[[{}, false, []]]], {}] ,'k' : false}", SINGLE);
        fpass(map, " [[[[{}, false, []]]], {}] ,'k' : false", SINGLE);
        fpass(map, ", [[[[{}, false, []]]], {}] ,'k' : false}\t,\r", SINGLE);
        fpass(map, ", 'testing' ,'k' : false", SINGLE);
        fpass(map, ", 'testing' ,'k' : false", SINGLE);
        fpass(map, ", 'testing' ,'k' : false", SINGLE);
        fpass(map, ", 'testing' ,'k' : false", SINGLE);
        fpass(map, ", 'testing' ,'k' : false", SINGLE);
        fpass(map, ", 'testing' ,'k' : false", SINGLE);

        List<Object> list = Lists.newArrayList();
        list.add(map);

        ffail(", 'testing' ,'k' : false]", SINGLE);
        fpass(list, ", 'testing' ,'k' : false}] ,", SINGLE);
        ffail("[, 'testing' ,'k' : false}", SINGLE);
        list.add(100);
        fpass(list, ", 'testing' ,'k' : false} , 100", SINGLE);
        fpass(list, ", 'testing' ,'k' : false} , 100 ,", SINGLE);
        fpass(list, ", 'testing' ,'k' : false} , 100 ] ,", SINGLE);
        //        fpass(map, null, SINGLE);

        ffail("789 : false", SINGLE);
        ffail("789 : 'testing'", SINGLE);
        ffail(", null : true", SINGLE);
        ffail(", false : true", SINGLE);
        ffail("false , false : true", SINGLE);
        ffail("'too many', false , 'key' : true", SINGLE);
        ffail(", 'testing' ,true : false", SINGLE);
        ffail("'too many roots', 'testing' ,'t' : false", SINGLE);
        ffail("'too many roots', null}", SINGLE);
        ffail("false, null}", SINGLE);
        ffail("789 : false}", SINGLE);
        ffail("789 : 'testing'}", SINGLE);
        ffail(", null : true}", SINGLE);
        ffail(", false : true}", SINGLE);
        ffail("false , false : true}", SINGLE);
        ffail("'too many', false , 'key' : true}", SINGLE);
        ffail(", 'testing' ,true : false}", SINGLE);
        ffail("'too many roots', 'testing' ,'t' : false}", SINGLE);
        ffail("'too many roots', null}", SINGLE);
        ffail("false, null}", SINGLE);
        ffail("'too many roots', 'test'}", SINGLE);
    }

    @Test
    public void testParseFragmentList() throws IOException {
        List<Object> list = Lists.newArrayList();

        fpass(null, "");
        fpass(null, ",");
        fpass(null, "null");
        fpass(false, "false");
        fpass(true, "true");
        fpass(100, "100");
        fpass("test", "\"test\"\n\n");

        fpass(list, "]");
        ffail(",]");
        fpass(list, ",]", COMMAS);
        fpass(list, " ] ,");
        ffail(", ] ");
        fpass(list, ", ] ", COMMAS);
        ffail(", ] , ");
        fpass(list, ", ] , ", COMMAS);
        ffail(" [ ,");
        fpass(list, " [ ,", COMMAS);
        fpass(list, ", [ ");
        ffail(", [ , ");
        fpass(list, ", [ , ", COMMAS);
        fpass(list, "[],");
        fpass(list, ", [], ");

        list.add(true);

        fpass(list, "[true");
        fpass(list, "[true ,");
        fpass(list, " , [true");
        fpass(list, ", [true ,");
        fpass(list, "true]");
        fpass(list, " , true]");
        fpass(list, "true],");
        fpass(list, ",true] ,");

        list.add(false);

        fpass(list, "true, false");
        fpass(list, ", true, false");
        fpass(list, "true, false,");
        fpass(list, ",true, false,");
        fpass(list, "[true, false");
        fpass(list, "[true, false ,");
        fpass(list, ", [true, false");
        fpass(list, " ,[true, false ,");
        fpass(list, "true, false]");
        fpass(list, "true, false] ,");
        fpass(list, ", true, false]");
        fpass(list, " ,true, false] ,");
        fpass(list, "[true, false]");
        fpass(list, "[true, false] ,");
        fpass(list, ", [true, false]");
        fpass(list, " ,[true, false] ,");

        list = Arrays.<Object>asList(list);

        fpass(list, "[[true, false");
        fpass(list, ", [[true, false,");

        fpass(list, "true, false]]");
        fpass(list, ", true, false]] ,");
    }

    @Test
    public void testFragmentDeepList() throws IOException {
        int depth = 300000;
        StringBuilder sb = new StringBuilder(depth + 16);

        final List<Object> head = Lists.newArrayList();
        List<Object> tail = head;
        for (; depth > 0; depth--) {
            List<Object> list = Lists.newArrayListWithCapacity(1);
            tail.add(list);
            tail = list;
            sb.append("]");
        }

        head.add("test");
        sb.append(", \"test\"");
        assertJsonEqualsNoRecurse(head, JSONValue.parseFragment(sb, STRICT));
    }

    @Test
    public void testFragmentDeepList2() throws IOException {
        int depth = 300000;
        StringBuilder sb = new StringBuilder(depth + 16);

        final List<Object> head = Lists.newArrayList();
        sb.append("[");
        List<Object> tail = head;
        for (; depth > 0; depth--) {
            List<Object> list = Lists.newArrayListWithCapacity(1);
            tail.add(list);
            tail = list;
            sb.append("[");
        }

        tail.add("test");
        sb.append("\"test\"");
        assertJsonEqualsNoRecurse(head, JSONValue.parseFragment(sb, STRICT));
    }

    @Test
    public void testFragmentShallowList() throws IOException {
        int depth = 100;
        StringBuilder sb = new StringBuilder(depth + 16);

        final List<Object> head = Lists.newArrayList();
        List<Object> tail = head;
        for (; depth > 0; depth--) {
            List<Object> list = Lists.newArrayListWithCapacity(1);
            tail.add(list);
            tail = list;
            sb.append("]");
        }

        head.add("test");
        sb.append(", \"test\"");
        assertJsonEquals(head, JSONValue.parseFragment(sb, STRICT));
    }

    @Test
    public void testFragmentDeepMap() throws IOException {
        int depth = 300000;
        String key = "k";
        StringBuilder sb = new StringBuilder(depth + 16);

        sb.append(String.format("\"%s\" : ", key));
        final Map<String, Object> head = MapUtils.newMutableSingletonMap();
        Map<String, Object> tail = head;
        for (; depth > 0; depth--) {
            Map<String, Object> map = MapUtils.newMutableSingletonMap();
            tail.put(key, map);
            tail = map;
            sb.append(String.format("{ \"%s\" : ", key));
        }

        assertJsonEqualsNoRecurse(head, JSONValue.parseFragment(sb, STRICT));
    }

    @Test
    public void testFragmentShallowMap() throws IOException {
        int depth = 100;
        String key = "k";
        StringBuilder sb = new StringBuilder(depth + 16);

        sb.append(String.format("\"%s\" : ", key));
        final Map<String, Object> head = MapUtils.newMutableSingletonMap();
        Map<String, Object> tail = head;
        for (; depth > 0; depth--) {
            Map<String, Object> map = MapUtils.newMutableSingletonMap();
            tail.put(key, map);
            tail = map;
            sb.append(String.format("{ \"%s\" : ", key));
        }

        assertJsonEquals(head, JSONValue.parseFragment(sb, STRICT));
    }
}