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