IndentationTest.java

/*
 * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.glassfish.jaxb.runtime.v2.schemagen;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import junit.framework.TestCase;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;

/**
 * Test case for issue #1645: Indentation of JAXB_FORMATTED_OUTPUT was limited to eight levels.
 * 
 * This test verifies that the indentation fix correctly handles deep nesting beyond 8 levels.
 * 
 * @see <a href="https://github.com/eclipse-ee4j/jaxb-ri/issues/1645">Issue #1645</a>
 */
public class IndentationTest extends TestCase {

    /**
     * Nested structure for testing deep indentation
     */
    @XmlRootElement(name = "level0")
    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level0 {
        @XmlElement
        private Level1 level1;

        public Level0() {}
        
        public Level0(Level1 level1) {
            this.level1 = level1;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level1 {
        @XmlElement
        private Level2 level2;

        public Level1() {}
        
        public Level1(Level2 level2) {
            this.level2 = level2;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level2 {
        @XmlElement
        private Level3 level3;

        public Level2() {}
        
        public Level2(Level3 level3) {
            this.level3 = level3;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level3 {
        @XmlElement
        private Level4 level4;

        public Level3() {}
        
        public Level3(Level4 level4) {
            this.level4 = level4;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level4 {
        @XmlElement
        private Level5 level5;

        public Level4() {}
        
        public Level4(Level5 level5) {
            this.level5 = level5;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level5 {
        @XmlElement
        private Level6 level6;

        public Level5() {}
        
        public Level5(Level6 level6) {
            this.level6 = level6;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level6 {
        @XmlElement
        private Level7 level7;

        public Level6() {}
        
        public Level6(Level7 level7) {
            this.level7 = level7;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level7 {
        @XmlElement
        private Level8 level8;

        public Level7() {}
        
        public Level7(Level8 level8) {
            this.level8 = level8;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level8 {
        @XmlElement
        private Level9 level9;

        public Level8() {}
        
        public Level8(Level9 level9) {
            this.level9 = level9;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level9 {
        @XmlElement
        private Level10 level10;

        public Level9() {}
        
        public Level9(Level10 level10) {
            this.level10 = level10;
        }
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Level10 {
        @XmlElement
        private String content;

        public Level10() {}
        
        public Level10(String content) {
            this.content = content;
        }
    }

    /**
     * Recursive node for flexible depth testing
     */
    @XmlRootElement(name = "node")
    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Node {
        @XmlElement
        private Node child;
        
        @XmlElement
        private String value;

        public Node() {}
        
        public Node(Node child, String value) {
            this.child = child;
            this.value = value;
        }
    }

    /**
     * Test that verifies indentation works correctly for deep nesting (>8 levels).
     * This is the primary test for issue #1645.
     * 
     * This test uses ByteArrayOutputStream to ensure the bug manifests when using streams,
     * as the IndentingUTF8XmlOutput is specifically designed for OutputStream.
     */
    @Test
    public void testDeepIndentation() throws Exception {
        // Create a deeply nested structure (11 levels total, including root)
        Level10 level10 = new Level10("Deep Content");
        Level9 level9 = new Level9(level10);
        Level8 level8 = new Level8(level9);
        Level7 level7 = new Level7(level8);
        Level6 level6 = new Level6(level7);
        Level5 level5 = new Level5(level6);
        Level4 level4 = new Level4(level5);
        Level3 level3 = new Level3(level4);
        Level2 level2 = new Level2(level3);
        Level1 level1 = new Level1(level2);
        Level0 level0 = new Level0(level1);

        // Marshal with formatted output to ByteArrayOutputStream (uses UTF8XmlOutput path)
        JAXBContext jc = JAXBContext.newInstance(Level0.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        marshaller.marshal(level0, byteStream);
        
        // Convert bytes to String using UTF-8
        String result = byteStream.toString(StandardCharsets.UTF_8.name());
        System.out.println("Test output (ByteArrayOutputStream):");
        System.out.println(result);

        // Verify the structure has proper indentation
        // Each level should be indented with 4 spaces more than the previous
        assertTrue("Result should contain level0", result.contains("<level0>"));
        assertTrue("Result should contain level1", result.contains("<level1>"));
        assertTrue("Result should contain level2", result.contains("<level2>"));
        assertTrue("Result should contain level3", result.contains("<level3>"));
        assertTrue("Result should contain level4", result.contains("<level4>"));
        assertTrue("Result should contain level5", result.contains("<level5>"));
        assertTrue("Result should contain level6", result.contains("<level6>"));
        assertTrue("Result should contain level7", result.contains("<level7>"));
        assertTrue("Result should contain level8", result.contains("<level8>"));
        assertTrue("Result should contain level9", result.contains("<level9>"));
        assertTrue("Result should contain level10", result.contains("<level10>"));
        assertTrue("Result should contain content", result.contains("<content>Deep Content</content>"));

        // Verify proper indentation at different levels
        // Level 1 should have 4 spaces
        assertTrue("Level 1 should be indented with 4 spaces", 
                   result.contains("\n    <level1>"));
        
        // Level 8 should have 32 spaces (8 * 4)
        assertTrue("Level 8 should be indented with 32 spaces", 
                   result.contains("\n                                <level8>"));
        
        // Level 9 should have 36 spaces (9 * 4)
        assertTrue("Level 9 should be indented with 36 spaces", 
                   result.contains("\n                                    <level9>"));
        
        // Level 10 should have 40 spaces (10 * 4)
        assertTrue("Level 10 should be indented with 40 spaces", 
                   result.contains("\n                                        <level10>"));
        
        // Content should have 44 spaces (11 * 4)
        assertTrue("Content should be indented with 44 spaces", 
                   result.contains("\n                                            <content>"));
        
        // Compare with StringWriter result to ensure consistency
        StringWriter stringWriter = new StringWriter();
        marshaller.marshal(level0, stringWriter);
        String writerResult = stringWriter.toString();
        
        System.out.println("\nTest output (StringWriter):");
        System.out.println(writerResult);
        
        // Both outputs should be identical
        assertEquals("ByteArrayOutputStream and StringWriter should produce identical output", 
                     writerResult, result);
    }

    /**
     * Test indentation at exactly 8 levels (boundary condition).
     * Uses ByteArrayOutputStream to test the UTF8 output path.
     */
    @Test
    public void testEightLevelIndentation() throws Exception {
        // Create structure with exactly 8 nested levels
        Level8 level8 = new Level8(null);
        Level7 level7 = new Level7(level8);
        Level6 level6 = new Level6(level7);
        Level5 level5 = new Level5(level6);
        Level4 level4 = new Level4(level5);
        Level3 level3 = new Level3(level4);
        Level2 level2 = new Level2(level3);
        Level1 level1 = new Level1(level2);
        Level0 level0 = new Level0(level1);

        JAXBContext jc = JAXBContext.newInstance(Level0.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        
        // Test with ByteArrayOutputStream
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        marshaller.marshal(level0, byteStream);
        String result = byteStream.toString(StandardCharsets.UTF_8.name());
        
        System.out.println("8-level test output (ByteArrayOutputStream):");
        System.out.println(result);

        // Level 8 should have 32 spaces (8 * 4)
        assertTrue("Level 8 should be indented with 32 spaces", 
                   result.contains("\n                                <level8"));
        
        // Compare with StringWriter result
        StringWriter stringWriter = new StringWriter();
        marshaller.marshal(level0, stringWriter);
        String writerResult = stringWriter.toString();
        
        System.out.println("\n8-level test output (StringWriter):");
        System.out.println(writerResult);
        
        assertEquals("ByteArrayOutputStream and StringWriter should produce identical output", 
                     writerResult, result);
    }

    /**
     * Test indentation with 16 levels (double the buffer size).
     * Uses ByteArrayOutputStream to test the UTF8 output path.
     */
    @Test
    public void testSixteenLevelIndentation() throws Exception {
        // Create 16 nested levels
        Node node = new Node(null, "level16");
        for (int i = 15; i >= 1; i--) {
            node = new Node(node, "level" + i);
        }

        JAXBContext jc = JAXBContext.newInstance(Node.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        
        // Test with ByteArrayOutputStream
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        marshaller.marshal(node, byteStream);
        String result = byteStream.toString(StandardCharsets.UTF_8.name());
        
        System.out.println("16-level test output (ByteArrayOutputStream, first 2000 chars):");
        System.out.println(result.substring(0, Math.min(2000, result.length())));

        // Verify deep indentation at level 16
        // Level 16 should have proper indentation (not collapsed)
        // We check that there are multiple levels of indentation present
        assertTrue("Result should contain node elements", result.contains("<node>"));
        assertTrue("Result should contain child elements", result.contains("<child>"));
        
        // Count the number of nested child elements to verify structure
        int childCount = 0;
        int index = 0;
        while ((index = result.indexOf("<child>", index)) != -1) {
            childCount++;
            index++;
        }
        
        assertTrue("Should have at least 15 child elements", childCount >= 15);
        
        // Compare with StringWriter result
        StringWriter stringWriter = new StringWriter();
        marshaller.marshal(node, stringWriter);
        String writerResult = stringWriter.toString();
        
        System.out.println("\n16-level test output (StringWriter, first 2000 chars):");
        System.out.println(writerResult.substring(0, Math.min(2000, writerResult.length())));
        
        assertEquals("ByteArrayOutputStream and StringWriter should produce identical output", 
                     writerResult, result);
    }
}