View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.utils.xml;
20  
21  import java.io.IOException;
22  import java.io.PrintWriter;
23  import java.io.Writer;
24  import java.util.ArrayList;
25  
26  /**
27   * XMLWriter with nice indentation.
28   *
29   * @author kama
30   */
31  public class PrettyPrintXMLWriter implements XMLWriter {
32      private static final char[] CLOSE_1 = "/>".toCharArray();
33  
34      private static final char[] CLOSE_2 = "</".toCharArray();
35  
36      private static final char[] DEFAULT_LINE_INDENT = new char[] {' ', ' '};
37  
38      private PrintWriter writer;
39  
40      private ArrayList<String> elementStack = new ArrayList<String>();
41  
42      private boolean processingElement = false;
43  
44      private boolean documentStarted = false;
45  
46      private boolean endOnSameLine = false;
47  
48      private int depth = 0;
49  
50      private char[] lineIndent;
51  
52      private char[] lineSeparator;
53  
54      private String encoding;
55  
56      private String docType;
57  
58      /**
59       * @param writer not null
60       * @param lineIndent can be null, but the normal way is some spaces
61       */
62      public PrettyPrintXMLWriter(PrintWriter writer, String lineIndent) {
63          this(writer, lineIndent, null, null);
64      }
65  
66      /**
67       * @param writer not null
68       * @param lineIndent can be null, but the normal way is some spaces.
69       */
70      public PrettyPrintXMLWriter(Writer writer, String lineIndent) {
71          this(new PrintWriter(writer), lineIndent);
72      }
73  
74      /**
75       * @param writer not null
76       */
77      public PrettyPrintXMLWriter(PrintWriter writer) {
78          this(writer, null, null);
79      }
80  
81      /**
82       * @param writer not null
83       */
84      public PrettyPrintXMLWriter(Writer writer) {
85          this(new PrintWriter(writer));
86      }
87  
88      /**
89       * @param writer not null
90       * @param lineIndent can be null, but the normal way is some spaces
91       * @param encoding can be null or invalid
92       * @param doctype can be null
93       */
94      public PrettyPrintXMLWriter(PrintWriter writer, String lineIndent, String encoding, String doctype) {
95          this(writer, lineIndent.toCharArray(), "\n".toCharArray(), encoding, doctype);
96      }
97  
98      /**
99       * @param writer not null
100      * @param lineIndent can be null, but the normal way is some spaces
101      * @param encoding can be null or invalid
102      * @param doctype can be null
103      */
104     public PrettyPrintXMLWriter(Writer writer, String lineIndent, String encoding, String doctype) {
105         this(new PrintWriter(writer), lineIndent, encoding, doctype);
106     }
107 
108     /**
109      * @param writer not null
110      * @param encoding can be null or invalid
111      * @param doctype can be null
112      */
113     public PrettyPrintXMLWriter(PrintWriter writer, String encoding, String doctype) {
114         this(writer, DEFAULT_LINE_INDENT, "\n".toCharArray(), encoding, doctype);
115     }
116 
117     /**
118      * @param writer not null
119      * @param encoding can be null or invalid
120      * @param doctype can be null
121      */
122     public PrettyPrintXMLWriter(Writer writer, String encoding, String doctype) {
123         this(new PrintWriter(writer), encoding, doctype);
124     }
125 
126     /**
127      * @param writer not null
128      * @param lineIndent can be null, but the normal way is some spaces.
129      * @param lineSeparator can be null, but the normal way is valid line separator
130      * @param encoding can be null or the encoding to use.
131      * @param doctype can be null
132      */
133     public PrettyPrintXMLWriter(
134             PrintWriter writer, String lineIndent, String lineSeparator, String encoding, String doctype) {
135         this(writer, lineIndent.toCharArray(), lineSeparator.toCharArray(), encoding, doctype);
136     }
137 
138     /**
139      * @param writer        not null
140      * @param lineIndent    can be null, but the normal way is some spaces
141      * @param lineSeparator can be null, but the normal way is valid line separator
142      * @param encoding      can be null or the encoding to use
143      * @param doctype       can be null
144      */
145     private PrettyPrintXMLWriter(
146             PrintWriter writer, char[] lineIndent, char[] lineSeparator, String encoding, String doctype) {
147         super();
148         this.writer = writer;
149         this.lineIndent = lineIndent;
150         this.lineSeparator = lineSeparator;
151         this.encoding = encoding;
152         this.docType = doctype;
153 
154         depth = 0;
155 
156         // Fail early with assertions enabled. Issue is in the calling code not having checked for any errors.
157         assert !writer.checkError() : "Unexpected error state PrintWriter passed to PrettyPrintXMLWriter.";
158     }
159 
160     /** {@inheritDoc} */
161     public void addAttribute(String key, String value) throws IOException {
162         if (!processingElement) {
163             throw new IllegalStateException("currently processing no element");
164         }
165 
166         writer.write(' ');
167         writer.write(key);
168         writer.write('=');
169         XMLEncode.xmlEncodeTextAsPCDATA(value, true, '"', writer);
170         if (writer.checkError()) {
171             throw new IOException("Failure adding attribute '" + key + "' with value '" + value + "'");
172         }
173     }
174 
175     /** {@inheritDoc} */
176     public void setEncoding(String encoding) {
177         if (documentStarted) {
178             throw new IllegalStateException("Document headers already written!");
179         }
180 
181         this.encoding = encoding;
182     }
183 
184     /** {@inheritDoc} */
185     public void setDocType(String docType) {
186         if (documentStarted) {
187             throw new IllegalStateException("Document headers already written!");
188         }
189 
190         this.docType = docType;
191     }
192 
193     /**
194      * @param lineSeparator the line separator to be output
195      */
196     public void setLineSeparator(String lineSeparator) {
197         if (documentStarted) {
198             throw new IllegalStateException("Document headers already written!");
199         }
200 
201         this.lineSeparator = lineSeparator.toCharArray();
202     }
203 
204     /**
205      * @param lineIndentParameter the line indent parameter
206      */
207     public void setLineIndenter(String lineIndentParameter) {
208         if (documentStarted) {
209             throw new IllegalStateException("Document headers already written!");
210         }
211 
212         this.lineIndent = lineIndentParameter.toCharArray();
213     }
214 
215     /** {@inheritDoc} */
216     public void startElement(String elementName) throws IOException {
217 
218         if (elementName.isEmpty()) {
219             throw new IllegalArgumentException("Element name cannot be empty");
220         }
221 
222         boolean firstLine = ensureDocumentStarted();
223 
224         completePreviouslyOpenedElement();
225 
226         if (!firstLine) {
227             newLine();
228         }
229 
230         writer.write('<');
231         writer.write(elementName);
232         if (writer.checkError()) {
233             throw new IOException("Failure starting element '" + elementName + "'.");
234         }
235 
236         processingElement = true;
237 
238         elementStack.add(depth++, elementName);
239     }
240 
241     /** {@inheritDoc} */
242     public void writeText(String text) throws IOException {
243         ensureDocumentStarted();
244 
245         completePreviouslyOpenedElement();
246 
247         XMLEncode.xmlEncodeText(text, writer);
248 
249         endOnSameLine = true;
250 
251         if (writer.checkError()) {
252             throw new IOException("Failure writing text.");
253         }
254     }
255 
256     /** {@inheritDoc} */
257     public void writeMarkup(String markup) throws IOException {
258         ensureDocumentStarted();
259 
260         completePreviouslyOpenedElement();
261 
262         writer.write(markup);
263 
264         if (writer.checkError()) {
265             throw new IOException("Failure writing markup.");
266         }
267     }
268 
269     /** {@inheritDoc} */
270     public void endElement() throws IOException {
271         String chars = elementStack.get(--depth);
272         if (processingElement) {
273             // this means we don't have any content yet so we just add a />
274             writer.write(CLOSE_1);
275 
276             processingElement = false;
277         } else {
278             if (!endOnSameLine) {
279                 newLine();
280             }
281 
282             // otherwise we need a full closing tag for that element
283             writer.write(CLOSE_2);
284             writer.write(chars);
285             writer.write('>');
286         }
287 
288         endOnSameLine = false;
289 
290         if (writer.checkError()) {
291             throw new IOException("Failure ending element.");
292         }
293     }
294 
295     /**
296      * Write the document if not already done.
297      *
298      * @return <code>true</code> if the document headers have freshly been written.
299      */
300     private boolean ensureDocumentStarted() {
301         if (!documentStarted) {
302             if (docType != null || encoding != null) {
303                 writeDocumentHeader();
304             }
305 
306             documentStarted = true;
307 
308             return true;
309         }
310 
311         return false;
312     }
313 
314     private void writeDocumentHeader() {
315         writer.write("<?xml version=\"1.0\"");
316 
317         if (encoding != null) {
318             writer.write(" encoding=\"");
319             writer.write(encoding);
320             writer.write('\"');
321         }
322 
323         writer.write("?>");
324 
325         newLine();
326 
327         if (docType != null) {
328             writer.write("<!DOCTYPE ");
329             writer.write(docType);
330             writer.write('>');
331             newLine();
332         }
333     }
334 
335     private void newLine() {
336         writer.write(lineSeparator);
337 
338         for (int i = 0; i < depth; i++) {
339             writer.write(lineIndent);
340         }
341     }
342 
343     private void completePreviouslyOpenedElement() {
344         if (processingElement) {
345             writer.write('>');
346             processingElement = false;
347         }
348     }
349 }