PrettyPrinterTest.java

package tools.jackson.core.unittest.write;

import java.io.*;

import org.junit.jupiter.api.Test;

import tools.jackson.core.*;
import tools.jackson.core.io.SerializedString;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.unittest.*;
import tools.jackson.core.util.*;
import tools.jackson.core.util.Separators.Spacing;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Set of basic unit tests for verifying that indenting
 * option of generator works correctly
 */
@SuppressWarnings("serial")
class PrettyPrinterTest
    extends JacksonCoreTestBase
{
    static class CountPrinter extends MinimalPrettyPrinter
    {
        @Override
        public void writeEndObject(JsonGenerator jg, int nrOfEntries)
        {
            jg.writeRaw("("+nrOfEntries+")}");
        }

        @Override
        public void writeEndArray(JsonGenerator jg, int nrOfValues)
        {
            jg.writeRaw("("+nrOfValues+")]");
        }
    }

    /*
    /**********************************************************
    /* Test methods
    /**********************************************************
     */

    private final JsonFactory JSON_F = newStreamFactory();

    @Test
    void objectCount() throws Exception
    {
        final String EXP = "{\"x\":{\"a\":1,\"b\":2(2)}(1)}";

        for (int i = 0; i < 2; ++i) {
            boolean useBytes = (i > 0);
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            StringWriter sw = new StringWriter();

            ObjectWriteContext ppContext = new ObjectWriteContext.Base() {
                @Override
                public PrettyPrinter getPrettyPrinter() { return new CountPrinter(); }
            };

            JsonGenerator gen = useBytes ? JSON_F.createGenerator(ppContext, bytes)
                    : JSON_F.createGenerator(ppContext, sw);
            gen.writeStartObject();
            gen.writeName("x");
            gen.writeStartObject();
            gen.writeNumberProperty("a", 1);
            gen.writeNumberProperty("b", 2);
            gen.writeEndObject();
            gen.writeEndObject();
            gen.close();

            String json = useBytes ? bytes.toString("UTF-8") : sw.toString();
            assertEquals(EXP, json);
        }
    }

    @Test
    void arrayCount() throws Exception
    {
        final String EXP = "[6,[1,2,9(3)](2)]";

        for (int i = 0; i < 2; ++i) {
            boolean useBytes = (i > 0);
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            StringWriter sw = new StringWriter();
            ObjectWriteContext ppContext = new ObjectWriteContext.Base() {
                @Override
                public PrettyPrinter getPrettyPrinter() { return new CountPrinter(); }
            };

            JsonGenerator gen = useBytes ? JSON_F.createGenerator(ppContext, bytes)
                    : JSON_F.createGenerator(ppContext, sw);
            gen.writeStartArray();
            gen.writeNumber(6);
            gen.writeStartArray();
            gen.writeNumber(1);
            gen.writeNumber(2);
            gen.writeNumber(9);
            gen.writeEndArray();
            gen.writeEndArray();
            gen.close();

            String json = useBytes ? bytes.toString("UTF-8") : sw.toString();
            assertEquals(EXP, json);
        }
    }

    @SuppressWarnings("resource")
    @Test
    void simpleDocWithMinimal() throws Exception
    {
        final PrettyPrinter MINIMAL = new MinimalPrettyPrinter();
        StringWriter sw = new StringWriter();
        // first with standard minimal
        ObjectWriteContext ppContext = new ObjectWriteContext.Base() {
            @Override
            public PrettyPrinter getPrettyPrinter() { return MINIMAL; }
        };
        JsonGenerator gen = JSON_F.createGenerator(ppContext, sw);

        // Verify instance uses stateless PP
        assertSame(MINIMAL, gen.getPrettyPrinter());

        String docStr = _verifyPrettyPrinter(gen, sw);
        // which should have no linefeeds, tabs
        assertEquals(-1, docStr.indexOf('\n'));
        assertEquals(-1, docStr.indexOf('\t'));

        // And then with slightly customized variant
        ppContext = new ObjectWriteContext.Base() {
            @Override
            public PrettyPrinter getPrettyPrinter() {
                return new MinimalPrettyPrinter() {
                    @Override
                    // use TAB between array values
                    public void beforeArrayValues(JsonGenerator g)
                    {
                        g.writeRaw("\t");
                    }
                };
            }
        };

        gen = new JsonFactory().createGenerator(ppContext, sw);
        docStr = _verifyPrettyPrinter(gen, sw);
        assertEquals(-1, docStr.indexOf('\n'));
        assertTrue(docStr.indexOf('\t') >= 0);
        gen.close();
    }

    // [core#26]
    @Test
    void rootSeparatorWithoutPP() throws Exception
    {
        // no pretty-printing (will still separate root values with a space!)
        assertEquals("{} {} []", _generateRoot(JSON_F, null));
    }

    // [core#26]
    @Test
    void defaultRootSeparatorWithPP() throws Exception
    {
        assertEquals("{ } { } [ ]", _generateRoot(JSON_F, new DefaultPrettyPrinter()));
    }

    // [core#26]
    @Test
    void customRootSeparatorWithPPNew() throws Exception
    {
        Separators separators = Separators.createDefaultInstance()
                .withRootSeparator("|");
        DefaultPrettyPrinter pp = new DefaultPrettyPrinter(separators);
        assertEquals("{ }|{ }|[ ]", _generateRoot(JSON_F, pp));
    }

    // Alternative solution for [jackson-core#26]
    @Test
    void customRootSeparatorWithFactory() throws Exception
    {
        JsonFactory f = JsonFactory.builder()
                .rootValueSeparator("##")
                .build();
        StringWriter sw = new StringWriter();
        JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), sw);
        gen.writeNumber(13);
        gen.writeBoolean(false);
        gen.writeNull();
        gen.close();
        assertEquals("13##false##null", sw.toString());
    }

    @Test
    void customSeparatorsWithMinimal() throws Exception
    {
        StringWriter sw = new StringWriter();
        ObjectWriteContext ppContext = new ObjectWriteContext.Base() {
            @Override
            public PrettyPrinter getPrettyPrinter() {
                return new MinimalPrettyPrinter().setSeparators(Separators.createDefaultInstance()
                        .withObjectNameValueSeparator('=')
                        .withObjectEntrySeparator(';')
                        .withArrayElementSeparator('|'));
            }
        };
        JsonGenerator gen = JSON_F.createGenerator(ppContext, sw);
        _writeTestDocument(gen);
        gen.close();
        assertEquals("[3|\"abc\"|[true]|{\"f\"=null;\"f2\"=null}]", sw.toString());

        // and with byte-backed too
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        gen = JSON_F.createGenerator(ppContext, bytes);
        _writeTestDocument(gen);
        gen.close();

        assertEquals("[3|\"abc\"|[true]|{\"f\"=null;\"f2\"=null}]", bytes.toString("UTF-8"));
    }

    @Test
    void customSeparatorsWithPP() throws Exception
    {
        StringWriter sw = new StringWriter();
        ObjectWriteContext ppContext = new ObjectWriteContext.Base() {
            @Override
            public PrettyPrinter getPrettyPrinter() {
                return new DefaultPrettyPrinter().withSeparators(Separators.createDefaultInstance()
                        .withObjectNameValueSeparator('=')
                        .withObjectEntrySeparator(';')
                        .withArrayElementSeparator('|'));
            }
        };

        JsonGenerator gen = new JsonFactory().createGenerator(ppContext, sw);
        _writeTestDocument(gen);
        gen.close();
        assertEquals("[ 3| \"abc\"| [ true ]| {" + DefaultIndenter.SYS_LF +
                "  \"f\" = null;" + DefaultIndenter.SYS_LF +
                "  \"f2\" = null" + DefaultIndenter.SYS_LF +
                "} ]", sw.toString());
    }

    private static final String EXPECTED_CUSTOM_SEPARATORS_WITH_PP_WITHOUT_SPACES =
            "[ 3| \"abc\"| [ true ]| {" + DefaultIndenter.SYS_LF +
            "  \"f\"=null;" + DefaultIndenter.SYS_LF +
            "  \"f2\"=null" + DefaultIndenter.SYS_LF +
            "} ]";

    @Test
    void customSeparatorsWithPPWithoutSpacesNew() throws Exception
    {
        final Separators separators = Separators.createDefaultInstance()
                .withObjectNameValueSeparator('=')
                .withObjectNameValueSpacing(Spacing.NONE)
                .withObjectEntrySeparator(';')
                .withArrayElementSeparator('|');
        ObjectWriteContext ppContext = new ObjectWriteContext.Base() {
            @Override
            public PrettyPrinter getPrettyPrinter() {
                return new DefaultPrettyPrinter(separators);
            }
        };

        StringWriter sw = new StringWriter();
        try (JsonGenerator gen = new JsonFactory().createGenerator(ppContext, sw)) {
            _writeTestDocument(gen);
        }
        assertEquals(EXPECTED_CUSTOM_SEPARATORS_WITH_PP_WITHOUT_SPACES, sw.toString());
    }

    // [core#1480]: access to configured/actual PrettyPrinter
    @Test
    void accessToPrettyPrinterInUse() throws Exception
    {
        // By default, no pretty-printer
        try (JsonGenerator gen = JSON_F.createGenerator(ObjectWriteContext.empty(),
                new StringWriter())) {
            assertNull(gen.getPrettyPrinter());
        }

        // But we can configure Default PP
        final PrettyPrinter DEFAULT_PP = new DefaultPrettyPrinter();
        try (JsonGenerator gen = JSON_F.createGenerator(objectWriteContext(DEFAULT_PP),
                new StringWriter())) {
            PrettyPrinter pp = gen.getPrettyPrinter();
            assertNotNull(pp);
            // Note: stateful, new instance created by our OWC
            assertEquals(DEFAULT_PP.getClass(), pp.getClass());
            assertNotSame(DEFAULT_PP, pp);
        }    
    }

    /*
    /**********************************************************
    /* Helper methods
    /**********************************************************
     */

    private String _verifyPrettyPrinter(JsonGenerator gen, StringWriter sw) throws Exception
    {
        _writeTestDocument(gen);

        String docStr = sw.toString();
        JsonParser jp = createParserUsingReader(docStr);

        assertEquals(JsonToken.START_ARRAY, jp.nextToken());

        assertEquals(JsonToken.VALUE_NUMBER_INT, jp.nextToken());
        assertEquals(3, jp.getIntValue());
        assertEquals(JsonToken.VALUE_STRING, jp.nextToken());
        assertEquals("abc", jp.getString());

        assertEquals(JsonToken.START_ARRAY, jp.nextToken());
        assertEquals(JsonToken.VALUE_TRUE, jp.nextToken());
        assertEquals(JsonToken.END_ARRAY, jp.nextToken());

        assertEquals(JsonToken.START_OBJECT, jp.nextToken());
        assertEquals(JsonToken.PROPERTY_NAME, jp.nextToken());
        assertEquals("f", jp.getString());
        assertEquals(JsonToken.VALUE_NULL, jp.nextToken());
        assertEquals(JsonToken.PROPERTY_NAME, jp.nextToken());
        assertEquals("f2", jp.getString());
        assertEquals(JsonToken.VALUE_NULL, jp.nextToken());
        assertEquals(JsonToken.END_OBJECT, jp.nextToken());

        assertEquals(JsonToken.END_ARRAY, jp.nextToken());

        jp.close();

        return docStr;
    }

    private void _writeTestDocument(JsonGenerator gen) throws IOException {
        gen.writeStartArray();
        gen.writeNumber(3);
        gen.writeString("abc");

        gen.writeStartArray();
        gen.writeBoolean(true);
        gen.writeEndArray();

        gen.writeStartObject();
        gen.writeName("f");
        gen.writeNull();
        // for better test coverage also use alt method
        gen.writeName(new SerializedString("f2"));
        gen.writeNull();
        gen.writeEndObject();

        gen.writeEndArray();
        gen.close();
    }

    protected String _generateRoot(TokenStreamFactory f, PrettyPrinter pp) throws IOException
    {
        StringWriter sw = new StringWriter();
        try (JsonGenerator gen = f.createGenerator(objectWriteContext(pp), sw)) {
            gen.writeStartObject();
            gen.writeEndObject();
            gen.writeStartObject();
            gen.writeEndObject();
            gen.writeStartArray();
            gen.writeEndArray();
        }
        return sw.toString();
    }

    protected ObjectWriteContext objectWriteContext(final PrettyPrinter pp) {
        return new ObjectWriteContext.Base() {
            @SuppressWarnings("unchecked")
            @Override
            public PrettyPrinter getPrettyPrinter() {
                if (pp instanceof Instantiatable) {
                    return ((Instantiatable<PrettyPrinter>) pp).createInstance();
                }
                return pp;
            }
        };
    }
}