View Javadoc
1   package org.codehaus.plexus.util.xml;
2   
3   /*
4    * Copyright The Codehaus Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * 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, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import java.io.PrintWriter;
20  import java.io.Writer;
21  import java.util.LinkedList;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  import org.codehaus.plexus.util.StringUtils;
26  
27  /**
28   * Implementation of XMLWriter which emits nicely formatted documents.
29   *
30   *
31   */
32  public class PrettyPrintXMLWriter implements XMLWriter {
33      /** Line separator ("\n" on UNIX) */
34      protected static final String LS = System.getProperty("line.separator");
35  
36      private PrintWriter writer;
37  
38      private LinkedList<String> elementStack = new LinkedList<String>();
39  
40      private boolean tagInProgress;
41  
42      private int depth;
43  
44      private String lineIndenter;
45  
46      private String lineSeparator;
47  
48      private String encoding;
49  
50      private String docType;
51  
52      private boolean readyForNewLine;
53  
54      private boolean tagIsEmpty;
55  
56      /**
57       * @param writer not null
58       * @param lineIndenter could be null, but the normal way is some spaces.
59       */
60      public PrettyPrintXMLWriter(PrintWriter writer, String lineIndenter) {
61          this(writer, lineIndenter, null, null);
62      }
63  
64      /**
65       * @param writer not null
66       * @param lineIndenter could be null, but the normal way is some spaces.
67       */
68      public PrettyPrintXMLWriter(Writer writer, String lineIndenter) {
69          this(new PrintWriter(writer), lineIndenter);
70      }
71  
72      /**
73       * @param writer not null
74       */
75      public PrettyPrintXMLWriter(PrintWriter writer) {
76          this(writer, null, null);
77      }
78  
79      /**
80       * @param writer not null
81       */
82      public PrettyPrintXMLWriter(Writer writer) {
83          this(new PrintWriter(writer));
84      }
85  
86      /**
87       * @param writer not null
88       * @param lineIndenter could be null, but the normal way is some spaces.
89       * @param encoding could be null or invalid.
90       * @param doctype could be null.
91       */
92      public PrettyPrintXMLWriter(PrintWriter writer, String lineIndenter, String encoding, String doctype) {
93          this(writer, lineIndenter, LS, encoding, doctype);
94      }
95  
96      /**
97       * @param writer not null
98       * @param lineIndenter 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(Writer writer, String lineIndenter, String encoding, String doctype) {
103         this(new PrintWriter(writer), lineIndenter, encoding, doctype);
104     }
105 
106     /**
107      * @param writer not null
108      * @param encoding could be null or invalid.
109      * @param doctype could be null.
110      */
111     public PrettyPrintXMLWriter(PrintWriter writer, String encoding, String doctype) {
112         this(writer, "  ", encoding, doctype);
113     }
114 
115     /**
116      * @param writer not null
117      * @param encoding could be null or invalid.
118      * @param doctype could be null.
119      */
120     public PrettyPrintXMLWriter(Writer writer, String encoding, String doctype) {
121         this(new PrintWriter(writer), encoding, doctype);
122     }
123 
124     /**
125      * @param writer not null
126      * @param lineIndenter could be null, but the normal way is some spaces.
127      * @param lineSeparator could be null, but the normal way is valid line separator ("\n" on UNIX).
128      * @param encoding could be null or invalid.
129      * @param doctype could be null.
130      */
131     public PrettyPrintXMLWriter(
132             PrintWriter writer, String lineIndenter, String lineSeparator, String encoding, String doctype) {
133         setWriter(writer);
134 
135         setLineIndenter(lineIndenter);
136 
137         setLineSeparator(lineSeparator);
138 
139         setEncoding(encoding);
140 
141         setDocType(doctype);
142 
143         if (doctype != null || encoding != null) {
144             writeDocumentHeaders();
145         }
146     }
147 
148     /** {@inheritDoc} */
149     @Override
150     public void startElement(String name) {
151         tagIsEmpty = false;
152 
153         finishTag();
154 
155         write("<");
156 
157         write(name);
158 
159         elementStack.addLast(name);
160 
161         tagInProgress = true;
162 
163         setDepth(getDepth() + 1);
164 
165         readyForNewLine = true;
166 
167         tagIsEmpty = true;
168     }
169 
170     /** {@inheritDoc} */
171     @Override
172     public void writeText(String text) {
173         writeText(text, true);
174     }
175 
176     /** {@inheritDoc} */
177     @Override
178     public void writeMarkup(String text) {
179         writeText(text, false);
180     }
181 
182     private void writeText(String text, boolean escapeXml) {
183         readyForNewLine = false;
184 
185         tagIsEmpty = false;
186 
187         finishTag();
188 
189         if (escapeXml) {
190             text = escapeXml(text);
191         }
192 
193         write(StringUtils.unifyLineSeparators(text, lineSeparator));
194     }
195 
196     private static final Pattern amp = Pattern.compile("&");
197 
198     private static final Pattern lt = Pattern.compile("<");
199 
200     private static final Pattern gt = Pattern.compile(">");
201 
202     private static final Pattern dqoute = Pattern.compile("\"");
203 
204     private static final Pattern sqoute = Pattern.compile("\'");
205 
206     private static String escapeXml(String text) {
207         if (text.indexOf('&') >= 0) {
208             text = amp.matcher(text).replaceAll("&amp;");
209         }
210         if (text.indexOf('<') >= 0) {
211             text = lt.matcher(text).replaceAll("&lt;");
212         }
213         if (text.indexOf('>') >= 0) {
214             text = gt.matcher(text).replaceAll("&gt;");
215         }
216         if (text.indexOf('"') >= 0) {
217             text = dqoute.matcher(text).replaceAll("&quot;");
218         }
219         if (text.indexOf('\'') >= 0) {
220             text = sqoute.matcher(text).replaceAll("&apos;");
221         }
222 
223         return text;
224     }
225 
226     private static final String crlf_str = "\r\n";
227 
228     private static final Pattern crlf = Pattern.compile(crlf_str);
229 
230     private static final Pattern lowers = Pattern.compile("([\000-\037])");
231 
232     private static String escapeXmlAttribute(String text) {
233         text = escapeXml(text);
234 
235         // Windows
236         Matcher crlfmatcher = crlf.matcher(text);
237         if (text.contains(crlf_str)) {
238             text = crlfmatcher.replaceAll("&#10;");
239         }
240 
241         Matcher m = lowers.matcher(text);
242         StringBuffer b = new StringBuffer();
243         while (m.find()) {
244             m = m.appendReplacement(b, "&#" + Integer.toString(m.group(1).charAt(0)) + ";");
245         }
246         m.appendTail(b);
247 
248         return b.toString();
249     }
250 
251     /** {@inheritDoc} */
252     @Override
253     public void addAttribute(String key, String value) {
254         write(" ");
255 
256         write(key);
257 
258         write("=\"");
259 
260         write(escapeXmlAttribute(value));
261 
262         write("\"");
263     }
264 
265     /** {@inheritDoc} */
266     @Override
267     public void endElement() {
268         setDepth(getDepth() - 1);
269 
270         if (tagIsEmpty) {
271             write("/");
272 
273             readyForNewLine = false;
274 
275             finishTag();
276 
277             elementStack.removeLast();
278         } else {
279             finishTag();
280 
281             // see issue #51: https://github.com/codehaus-plexus/plexus-utils/issues/51
282             // Rationale: replaced 1 write() with string concatenations with 3 write()
283             // (this avoids the string concatenation optimization bug detected in Java 7)
284             // TODO: change the below code to a more efficient expression when the library
285             // be ready to target Java 8.
286             write("</");
287             write(elementStack.removeLast());
288             write(">");
289         }
290 
291         readyForNewLine = true;
292     }
293 
294     /**
295      * Write a string to the underlying writer
296      *
297      * @param str
298      */
299     private void write(String str) {
300         getWriter().write(str);
301     }
302 
303     private void finishTag() {
304         if (tagInProgress) {
305             write(">");
306         }
307 
308         tagInProgress = false;
309 
310         if (readyForNewLine) {
311             endOfLine();
312         }
313         readyForNewLine = false;
314 
315         tagIsEmpty = false;
316     }
317 
318     /**
319      * Get the string used as line indenter
320      *
321      * @return the line indenter
322      */
323     protected String getLineIndenter() {
324         return lineIndenter;
325     }
326 
327     /**
328      * Set the string used as line indenter
329      *
330      * @param lineIndenter new line indenter, could be null, but the normal way is some spaces.
331      */
332     protected void setLineIndenter(String lineIndenter) {
333         this.lineIndenter = lineIndenter;
334     }
335 
336     /**
337      * Get the string used as line separator or LS if not set.
338      *
339      * @return the line separator
340      * @see #LS
341      */
342     protected String getLineSeparator() {
343         return lineSeparator;
344     }
345 
346     /**
347      * Set the string used as line separator
348      *
349      * @param lineSeparator new line separator, could be null but the normal way is valid line separator ("\n" on UNIX).
350      */
351     protected void setLineSeparator(String lineSeparator) {
352         this.lineSeparator = lineSeparator;
353     }
354 
355     /**
356      * Write the end of line character (using specified line separator) and start new line with indentation
357      *
358      * @see #getLineIndenter()
359      * @see #getLineSeparator()
360      */
361     protected void endOfLine() {
362         write(getLineSeparator());
363 
364         for (int i = 0; i < getDepth(); i++) {
365             write(getLineIndenter());
366         }
367     }
368 
369     private void writeDocumentHeaders() {
370         write("<?xml version=\"1.0\"");
371 
372         if (getEncoding() != null) {
373             write(" encoding=\"" + getEncoding() + "\"");
374         }
375 
376         write("?>");
377 
378         endOfLine();
379 
380         if (getDocType() != null) {
381             write("<!DOCTYPE ");
382 
383             write(getDocType());
384 
385             write(">");
386 
387             endOfLine();
388         }
389     }
390 
391     /**
392      * Set the underlying writer
393      *
394      * @param writer not null writer
395      */
396     protected void setWriter(PrintWriter writer) {
397         if (writer == null) {
398             throw new IllegalArgumentException("writer could not be null");
399         }
400 
401         this.writer = writer;
402     }
403 
404     /**
405      * Get the underlying writer
406      *
407      * @return the underlying writer
408      */
409     protected PrintWriter getWriter() {
410         return writer;
411     }
412 
413     /**
414      * Set the depth in the xml indentation
415      *
416      * @param depth new depth
417      */
418     protected void setDepth(int depth) {
419         this.depth = depth;
420     }
421 
422     /**
423      * Get the current depth in the xml indentation
424      *
425      * @return the current depth
426      */
427     protected int getDepth() {
428         return depth;
429     }
430 
431     /**
432      * Set the encoding in the xml
433      *
434      * @param encoding new encoding
435      */
436     protected void setEncoding(String encoding) {
437         this.encoding = encoding;
438     }
439 
440     /**
441      * Get the current encoding in the xml
442      *
443      * @return the current encoding
444      */
445     protected String getEncoding() {
446         return encoding;
447     }
448 
449     /**
450      * Set the docType in the xml
451      *
452      * @param docType new docType
453      */
454     protected void setDocType(String docType) {
455         this.docType = docType;
456     }
457 
458     /**
459      * Get the docType in the xml
460      *
461      * @return the current docType
462      */
463     protected String getDocType() {
464         return docType;
465     }
466 
467     /**
468      * @return the current elementStack;
469      */
470     protected LinkedList<String> getElementStack() {
471         return elementStack;
472     }
473 }