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