1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.schemagen.xml.impl;
7   
8   import java.util.ArrayDeque;
9   import java.util.Deque;
10  
11  import javax.xml.namespace.NamespaceContext;
12  import javax.xml.stream.XMLStreamException;
13  import javax.xml.stream.XMLStreamWriter;
14  
15  import edu.umd.cs.findbugs.annotations.NonNull;
16  
17  /**
18   * An XMLStreamWriter wrapper that adds indentation to the output.
19   * <p>
20   * This wrapper handles mixed content correctly by tracking when text has been
21   * written to an element. When an element contains text (mixed content), no
22   * indentation is added to preserve the text formatting.
23   * <p>
24   * This class is used to replace Saxon XSLT post-processing for schema
25   * indentation, providing streaming indentation without buffering the entire
26   * document.
27   */
28  public class IndentingXMLStreamWriter implements XMLStreamWriter, AutoCloseable {
29  
30    private static final String NEWLINE = "\n";
31    private static final String INDENT = "  ";
32  
33    @NonNull
34    private final XMLStreamWriter delegate;
35  
36    private int depth;
37    private final Deque<Boolean> hasTextStack = new ArrayDeque<>();
38    private boolean hasText;
39    private boolean lastWasStart;
40  
41    /**
42     * Constructs a new indenting XML stream writer.
43     *
44     * @param delegate
45     *          the underlying writer to delegate to
46     */
47    public IndentingXMLStreamWriter(@NonNull XMLStreamWriter delegate) {
48      this.delegate = delegate;
49      this.depth = 0;
50      this.hasText = false;
51      this.lastWasStart = false;
52    }
53  
54    /**
55     * Writes indentation at the current depth level.
56     *
57     * @throws XMLStreamException
58     *           if an error occurs writing
59     */
60    private void writeIndent() throws XMLStreamException {
61      delegate.writeCharacters(NEWLINE);
62      for (int i = 0; i < depth; i++) {
63        delegate.writeCharacters(INDENT);
64      }
65    }
66  
67    @Override
68    public void writeStartElement(String localName) throws XMLStreamException {
69      prepareStartElement();
70      delegate.writeStartElement(localName);
71      afterStartElement();
72    }
73  
74    @Override
75    public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException {
76      prepareStartElement();
77      delegate.writeStartElement(namespaceURI, localName);
78      afterStartElement();
79    }
80  
81    @Override
82    public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
83      prepareStartElement();
84      delegate.writeStartElement(prefix, localName, namespaceURI);
85      afterStartElement();
86    }
87  
88    /**
89     * Prepares for writing a start element by adding indentation if appropriate.
90     *
91     * @throws XMLStreamException
92     *           if an error occurs writing
93     */
94    private void prepareStartElement() throws XMLStreamException {
95      if (!hasText) {
96        writeIndent();
97      }
98      hasTextStack.push(hasText);
99    }
100 
101   /**
102    * Updates state after writing a start element.
103    */
104   private void afterStartElement() {
105     depth++;
106     hasText = false;
107     lastWasStart = true;
108   }
109 
110   @Override
111   public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException {
112     if (!hasText) {
113       writeIndent();
114     }
115     delegate.writeEmptyElement(namespaceURI, localName);
116     lastWasStart = false;
117   }
118 
119   @Override
120   public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
121     if (!hasText) {
122       writeIndent();
123     }
124     delegate.writeEmptyElement(prefix, localName, namespaceURI);
125     lastWasStart = false;
126   }
127 
128   @Override
129   public void writeEmptyElement(String localName) throws XMLStreamException {
130     if (!hasText) {
131       writeIndent();
132     }
133     delegate.writeEmptyElement(localName);
134     lastWasStart = false;
135   }
136 
137   @Override
138   public void writeEndElement() throws XMLStreamException {
139     depth--;
140     boolean parentHasText = !hasTextStack.isEmpty() && hasTextStack.pop();
141 
142     if (!hasText && !lastWasStart) {
143       writeIndent();
144     }
145     delegate.writeEndElement();
146     hasText = parentHasText;
147     lastWasStart = false;
148   }
149 
150   @Override
151   public void writeEndDocument() throws XMLStreamException {
152     delegate.writeEndDocument();
153   }
154 
155   @Override
156   public void close() throws XMLStreamException {
157     delegate.close();
158   }
159 
160   @Override
161   public void flush() throws XMLStreamException {
162     delegate.flush();
163   }
164 
165   @Override
166   public void writeAttribute(String localName, String value) throws XMLStreamException {
167     delegate.writeAttribute(localName, value);
168   }
169 
170   @Override
171   public void writeAttribute(String prefix, String namespaceURI, String localName, String value)
172       throws XMLStreamException {
173     delegate.writeAttribute(prefix, namespaceURI, localName, value);
174   }
175 
176   @Override
177   public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException {
178     delegate.writeAttribute(namespaceURI, localName, value);
179   }
180 
181   @Override
182   public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {
183     delegate.writeNamespace(prefix, namespaceURI);
184   }
185 
186   @Override
187   public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException {
188     delegate.writeDefaultNamespace(namespaceURI);
189   }
190 
191   @Override
192   public void writeComment(String data) throws XMLStreamException {
193     if (!hasText) {
194       writeIndent();
195     }
196     delegate.writeComment(data);
197     lastWasStart = false;
198   }
199 
200   @Override
201   public void writeProcessingInstruction(String target) throws XMLStreamException {
202     if (!hasText) {
203       writeIndent();
204     }
205     delegate.writeProcessingInstruction(target);
206     lastWasStart = false;
207   }
208 
209   @Override
210   public void writeProcessingInstruction(String target, String data) throws XMLStreamException {
211     if (!hasText) {
212       writeIndent();
213     }
214     delegate.writeProcessingInstruction(target, data);
215     lastWasStart = false;
216   }
217 
218   @Override
219   public void writeCData(String data) throws XMLStreamException {
220     delegate.writeCData(data);
221     hasText = true;
222     lastWasStart = false;
223   }
224 
225   @Override
226   public void writeDTD(String dtd) throws XMLStreamException {
227     delegate.writeDTD(dtd);
228   }
229 
230   @Override
231   public void writeEntityRef(String name) throws XMLStreamException {
232     delegate.writeEntityRef(name);
233     hasText = true;
234     lastWasStart = false;
235   }
236 
237   @Override
238   public void writeStartDocument() throws XMLStreamException {
239     delegate.writeStartDocument();
240   }
241 
242   @Override
243   public void writeStartDocument(String version) throws XMLStreamException {
244     delegate.writeStartDocument(version);
245   }
246 
247   @Override
248   public void writeStartDocument(String encoding, String version) throws XMLStreamException {
249     delegate.writeStartDocument(encoding, version);
250   }
251 
252   @Override
253   public void writeCharacters(String text) throws XMLStreamException {
254     delegate.writeCharacters(text);
255     hasText = true;
256     lastWasStart = false;
257   }
258 
259   @Override
260   public void writeCharacters(char[] text, int start, int len) throws XMLStreamException {
261     delegate.writeCharacters(text, start, len);
262     hasText = true;
263     lastWasStart = false;
264   }
265 
266   @Override
267   public String getPrefix(String uri) throws XMLStreamException {
268     return delegate.getPrefix(uri);
269   }
270 
271   @Override
272   public void setPrefix(String prefix, String uri) throws XMLStreamException {
273     delegate.setPrefix(prefix, uri);
274   }
275 
276   @Override
277   public void setDefaultNamespace(String uri) throws XMLStreamException {
278     delegate.setDefaultNamespace(uri);
279   }
280 
281   @Override
282   public void setNamespaceContext(NamespaceContext context) throws XMLStreamException {
283     delegate.setNamespaceContext(context);
284   }
285 
286   @Override
287   public NamespaceContext getNamespaceContext() {
288     return delegate.getNamespaceContext();
289   }
290 
291   @Override
292   public Object getProperty(String name) {
293     return delegate.getProperty(name);
294   }
295 }