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