Source code: org/progeeks/meta/xml/XmlPrintWriter.java
1 /*
2 * $Id: XmlPrintWriter.java,v 1.4 2003/09/22 05:45:26 pspeed Exp $
3 *
4 * Copyright (c) 2001-2002, Paul Speed
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * 1) Redistributions of source code must retain the above copyright notice,
12 * this list of conditions and the following disclaimer.
13 * 2) Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 * 3) Neither the names "Progeeks", "Meta-JB", nor the names of its contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 * POSSIBILITY OF SUCH DAMAGE.
31 */
32
33 package org.progeeks.meta.xml;
34
35 import java.io.*;
36 import java.util.*;
37
38
39 /**
40 * Extends the functionality of the IndentPrintWriter to keep
41 * track of tag state and provide attribute support, etc.. Also,
42 * all output is encoded as appropriate for its context.
43 *
44 * @version $Revision: 1.4 $
45 * @author Paul Speed
46 */
47 public class XmlPrintWriter extends IndentPrintWriter
48 {
49 private List tags = new LinkedList();
50
51 private static final int CTX_BODY = 0;
52 private static final int CTX_TAG = 1;
53 private static final int CTX_COMMENT = 2;
54 private static final int CTX_DATA = 3;
55
56 /**
57 * Holds the current context mode to keep track of whether
58 * or not we are in a tag, a comment, a CDATA block, or just
59 * normal body text.
60 */
61 private int context = CTX_BODY;
62
63 /**
64 * The name of the currently open tag or null if there
65 * is no open tag.
66 */
67 private String openTag = null;
68
69 /**
70 * True if attributes have been written for the current
71 * tag.
72 */
73 private boolean hasAttributes = false;
74
75 /**
76 * Set to true if the current tag is in single tag
77 * form, ie: <tag /> This is necessary for auto-closing
78 * the tag.
79 */
80 private boolean singleTag = false;
81
82
83 public XmlPrintWriter( Writer out )
84 {
85 super( out );
86 }
87
88 /**
89 * Prints the tag start and sets up the indent for nesting.
90 */
91 public void pushTag( String tag )
92 {
93 closeBlock();
94
95 tags.add( 0, tag );
96 print( "<" + tag );
97 context = CTX_TAG;
98 openTag = tag;
99 }
100
101 /**
102 * Prints the specified tag in single-element form, ie: <tag />
103 */
104 public void printTag( String tag )
105 {
106 closeBlock();
107 print( "<" + tag );
108 context = CTX_TAG;
109 singleTag = true;
110 openTag = tag;
111 }
112
113 /**
114 * Writes the specified attribute to the current tag if it is still
115 * open. Otherwise it will throw an IllegalStateException. The name
116 * will be verified for XML attribute name restrictions and the
117 * value will be encoded as necessary for attribute values.
118 *
119 * @param name The name of the attribute.
120 * @param value The value to be written. If the value is null then
121 * the attribute will not be written.
122 */
123 public void printAttribute( String name, String value )
124 {
125 if( value == null )
126 return;
127
128 if( context != CTX_TAG )
129 {
130 throw new IllegalStateException( "No open tags to receive attributes." );
131 }
132
133 // Check the name
134
135 // Encode the value
136 value = encodeXml( value, true );
137
138 if( !hasAttributes )
139 print( " " );
140
141 print( name + "=\"" + value + "\" " );
142
143 hasAttributes = true;
144 }
145
146 /**
147 * Prints the closing element for the current tag and pops the indent.
148 */
149 public String popTag()
150 {
151 if( tags.size() == 0 )
152 throw new IllegalStateException( "Tag stack is empty." );
153 String tag = (String)tags.remove( 0 );
154
155 // If we're still the same tag mode, then we've never written
156 // anything inside the tag and we can close the tag better
157 // as if it were a non-nested tag.
158 if( context == CTX_TAG && openTag == tag )
159 {
160 singleTag = true;
161 closeBlock();
162 }
163 else
164 {
165 closeBlock();
166
167 popIndent();
168 println( "</" + tag + ">" );
169 }
170
171 return( tag );
172 }
173
174 /**
175 * Prints the closing elements for the stacked tags until the
176 * specified tag is reached and popped. If the tag is never
177 * found than an illegal state exception is thrown.
178 */
179 public void popTag( String tag )
180 {
181 while( !tag.equals( popTag() ) )
182 ;
183 }
184
185 /**
186 * Prints the specified string as an enclosed comment.
187 */
188 public void printComment( String comment )
189 {
190 startComment();
191 print( comment );
192 closeComment();
193 }
194
195 /**
196 * Starts a comment block.
197 */
198 public void startComment()
199 {
200 closeBlock();
201 print( "<!-- " );
202
203 context = CTX_COMMENT;
204
205 // Push an indent so that subsequent lines will line
206 // up with the first. Five spaces are needed for that.
207 pushIndent( " " );
208 }
209
210 /**
211 * Closes the comment block.
212 */
213 public void closeComment()
214 {
215 if( context != CTX_COMMENT )
216 return;
217 popIndent();
218 println( " -->" );
219
220 context = CTX_BODY;
221 }
222
223 /**
224 * Starts a CDATA block.
225 */
226 public void startDataBlock()
227 {
228 closeBlock();
229 setOverrideIndent( true );
230 println( "<![CDATA[" );
231
232 context = CTX_DATA;
233 }
234
235 /**
236 * Closes a CDATA block.
237 */
238 public void closeDataBlock()
239 {
240 if( context != CTX_DATA )
241 return;
242 println( "]]>" );
243 setOverrideIndent( false );
244
245 context = CTX_BODY;
246 }
247
248 /**
249 * Closes the current tag or the current comment block
250 * depending on what block is currently open.
251 */
252 protected void closeBlock()
253 {
254 switch( context )
255 {
256 default:
257 case CTX_BODY:
258 break;
259 case CTX_TAG:
260 closeTag();
261 break;
262 case CTX_COMMENT:
263 closeComment();
264 break;
265 case CTX_DATA:
266 closeDataBlock();
267 break;
268 }
269 }
270
271 /**
272 * Closes the current tag if open. Otherwise this is a noop.
273 * Note this is not to be confused with popTag() which actually
274 * writes a closing tag in many cases.
275 */
276 protected void closeTag()
277 {
278 if( context != CTX_TAG )
279 return;
280
281 if( !singleTag )
282 println( ">" );
283 else if( hasAttributes )
284 println( "/>" );
285 else
286 println( " />" );
287
288 // Auto-indent for standard tags
289 if( !singleTag )
290 {
291 pushIndent();
292 }
293
294 context = CTX_BODY;
295 singleTag = false;
296 hasAttributes = false;
297 openTag = null;
298 }
299
300 /**
301 * Automatically finished writing any in-progess tags and then
302 * closes the file.
303 */
304 public void close()
305 {
306 while( tags.size() > 0 )
307 popTag();
308
309 super.close();
310 }
311
312 /**
313 * Overridden to provide XML encoding when appropriate.
314 */
315 public void write( int c )
316 {
317 writeEncoded( c );
318 }
319
320 /**
321 * Overridden to provide XML encoding when appropriate.
322 */
323 public void write( char[] buff, int off, int len )
324 {
325 switch( context )
326 {
327 case CTX_BODY:
328 case CTX_COMMENT:
329 // Only provide low-level encoding for body and comment
330 // text.
331 // Gotta do it the slow way
332 for( int i = 0; i < len; i++ )
333 {
334 writeEncoded( buff[ off + i ] );
335 }
336 return;
337 case CTX_TAG:
338 case CTX_DATA:
339 default:
340 break;
341 }
342 super.write( buff, off, len );
343 }
344
345 /**
346 * Overridden to provide XML encoding when appropriate.
347 */
348 public void write( String s, int off, int len )
349 {
350 switch( context )
351 {
352 case CTX_BODY:
353 case CTX_COMMENT:
354 // Only provide low-level encoding for body and comment
355 // text.
356 // Gotta do it the slow way
357 for( int i = 0; i < len; i++ )
358 {
359 char c = s.charAt( off + i );
360 writeEncoded( c );
361 }
362 return;
363 case CTX_TAG:
364 case CTX_DATA:
365 default:
366 break;
367 }
368
369 super.write( s, off, len );
370 }
371
372 /**
373 * Returns true if the specified character should be encoded.
374 */
375 protected boolean isEncodable( int c )
376 {
377 // Right now just the one
378 if( c == '&' )
379 return( true );
380 return( false );
381 }
382
383 /**
384 * Writes the specified value out as an encoded string if
385 * necessary.
386 */
387 protected void writeEncoded( int c )
388 {
389 if( c == '&' )
390 {
391 super.write( "&", 0, 5 );
392 }
393 else
394 {
395 super.write( c );
396 }
397 }
398
399 /**
400 * Returns an encoded safe string for the specified value.
401 * If quoted is true then it is also safe to encode quotes
402 * and <> signs.
403 */
404 private static String encodeXml( String value, boolean quoted )
405 {
406 value = value.replaceAll( "&", "&" );
407 if( quoted )
408 {
409 value = value.replaceAll( "\"", """ );
410 value = value.replaceAll( "'", "'" );
411 value = value.replaceAll( "<", "<" );
412 value = value.replaceAll( ">", ">" );
413 }
414 return( value );
415 }
416
417 public static void main( String[] args ) throws IOException
418 {
419 OutputStreamWriter osw = new OutputStreamWriter( System.out );
420 XmlPrintWriter out = new XmlPrintWriter( osw );
421
422 out.pushTag( "root" );
423
424 out.startComment();
425 out.println( "This is a test of a comment block." );
426 out.println( "This is the second line of the test." );
427 out.println( "This line requires encoding the & symbol." );
428
429 out.pushTag( "firstInner" );
430 out.printAttribute( "test", "A test attribute." );
431 out.printTag( "single" );
432 out.printTag( "anotherSingle" );
433 out.printAttribute( "foo", "Bar" );
434 out.printAttribute( "bar", "Foo" );
435
436 out.popTag();
437
438 out.println( "This is just some body text floating around in the" );
439 out.println( "middle of nowhere. And it has an & symbol too." );
440
441 out.startDataBlock();
442 out.println( "This is just some raw data that is not XML at all." );
443 out.println( "This is how you would include raw data in an XML file." );
444
445 out.pushTag( "secondInner" );
446 out.pushTag( "nestedInner" );
447
448 out.popTag( "secondInner" );
449
450 out.startComment();
451 out.print( "We should be in the root tag now." );
452 out.closeComment();
453
454 out.close();
455 }
456 }
457
458