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