001package com.hfg.xml;
002
003import java.io.OutputStream;
004import java.io.PrintWriter;
005import java.util.ArrayList;
006import java.util.List;
007import java.util.Stack;
008
009import org.xml.sax.Attributes;
010import org.xml.sax.SAXException;
011import org.xml.sax.SAXParseException;
012import org.xml.sax.ext.LexicalHandler;
013import org.xml.sax.helpers.DefaultHandler;
014
015import com.hfg.util.StringUtil;
016
017//------------------------------------------------------------------------------
018/**
019  
020  @author J. Alex Taylor, hairyfatguy.com
021 */
022//------------------------------------------------------------------------------
023
024
025public class XMLEmitter extends DefaultHandler implements LexicalHandler
026{
027
028   //###########################################################################
029   // PUBLIC FIELDS
030   //###########################################################################
031
032   public static final int STYLE_COMPACT  = 1;
033   public static final int STYLE_INDENTED = 2;
034   
035   //###########################################################################
036   // PRIVATE FIELDS
037   //###########################################################################
038
039   private StringBuffer mCurrentData;
040   private StringBuffer mCurrentComment;
041   private int          mPreviousTagType;
042   private int          mCurrentIndent;
043   private XMLTag       mDanglingStartTag;
044
045   private PrintWriter  mWriter;
046   private int          mPrintStyle  = STYLE_COMPACT;
047   private int          mIndentSize  = 2;
048
049   private Stack        mSafeToIndentStack = new Stack();
050   private boolean      mSafeToIndent;
051
052   private static int   START_TAG    = 1;
053   private static int   END_TAG      = -1;
054
055   //###########################################################################
056   // CONSTRUCTORS
057   //###########################################################################
058     
059   //---------------------------------------------------------------------------
060   /**
061     Default constructor - prints to System.out
062    */
063   public XMLEmitter()
064   {
065      mWriter = new PrintWriter(System.out);
066   }   
067     
068   //---------------------------------------------------------------------------
069   /**
070     Prints XML to the specified stream
071    */
072   public XMLEmitter(OutputStream inOutputStream)
073   {
074      mWriter = new PrintWriter(inOutputStream);
075   }
076
077   //---------------------------------------------------------------------------
078   /**
079     Prints XML to the specified stream
080    */
081   public XMLEmitter(PrintWriter inWriter)
082   {
083      mWriter = inWriter;
084   }
085
086   
087   //###########################################################################
088   // PUBLIC METHODS
089   //###########################################################################
090
091   //---------------------------------------------------------------------------
092   public void setPrintStyle(int inValue)
093   {
094      if (inValue < STYLE_COMPACT 
095          || inValue > STYLE_INDENTED)
096      {
097         throw new RuntimeException(inValue + " is not a valid print style!");
098      }
099      
100      mPrintStyle = inValue;
101   }
102
103   //---------------------------------------------------------------------------
104   /**
105     Sets the number of spaces each tag level is indented
106    */
107   public void setIndentSize(int inValue)
108   {
109      mIndentSize = inValue;
110   }
111
112   //---------------------------------------------------------------------------
113   public int getIndentSize()
114   {
115      return mIndentSize;
116   }
117   
118   //---------------------------------------------------------------------------
119   public void startDocument()
120   {
121      mCurrentData = new StringBuffer();
122      mCurrentComment = new StringBuffer();
123      mPreviousTagType = END_TAG;
124      mCurrentIndent = -mIndentSize;
125
126      mSafeToIndentStack = new Stack();
127      mSafeToIndent = true;
128   }
129
130   //---------------------------------------------------------------------------
131   public void endDocument()
132   {
133      mWriter.flush();
134   }
135
136   //---------------------------------------------------------------------------
137   public void startElement(String uri, String local, String raw, Attributes attrs)
138   {
139      if (mDanglingStartTag != null)
140      {
141         emitStartTag(false);
142
143         if (mCurrentData.length() > 0)
144         {
145             emitContent();
146         }
147      }
148
149      List attributes = new ArrayList();
150      for (int i = 0; i < attrs.getLength(); i++)
151      {
152         attributes.add(new XMLAttribute(attrs.getQName(i), attrs.getValue(i)));
153      }
154
155      mDanglingStartTag = new XMLTag(raw, attributes);
156
157/*
158      boolean safeToIndent = true;
159
160      if (mCurrentData.length() > 0)
161      {
162         if (mPrintStyle == STYLE_INDENTED)
163         {  
164                safeToIndent = contentLooksSafeToIndent(mCurrentData.toString());
165
166            if (safeToIndent) 
167            {
168               mPrintStream.println();
169               mPrintStream.print(StringUtil.polyChar(' ', mCurrentIndent + mIndentSize));
170            }
171         }
172         
173         mPrintStream.print(mCurrentData.toString());
174
175         if (mPrintStyle == STYLE_INDENTED && safeToIndent) mPrintStream.println();           
176      } 
177      
178   
179      List attributes = new ArrayList();
180      for (int i = 0; i < attrs.getLength(); i++)
181      {
182         attributes.add(new XMLAttribute(attrs.getQName(i), attrs.getValue(i)));
183      }
184      
185      if (mPrintStyle == STYLE_INDENTED)
186      {
187         mCurrentIndent += mIndentSize;
188         mPrintStream.println();
189         mPrintStream.print(StringUtil.polyChar(' ', mCurrentIndent));
190         mPreviousTagType = START_TAG;
191      }
192      
193      mPrintStream.print(XMLUtil.composeStartTag(raw, attributes));
194            
195      mCurrentData = new StringBuffer();
196*/
197   }
198
199   //---------------------------------------------------------------------------
200   public void endElement(String uri, String local, String raw)
201   {
202/*
203      // Print any tag contents
204      if (mCurrentData.length() > 0)
205      {
206         if (mPrintStyle == STYLE_INDENTED)
207         {  
208                safeToIndent = contentLooksSafeToIndent(mCurrentData.toString());
209
210            if (safeToIndent) 
211            {
212               mPrintStream.println();
213               mPrintStream.print(StringUtil.polyChar(' ', mCurrentIndent + mIndentSize));
214            }
215         }
216         
217         mPrintStream.print(mCurrentData.toString());
218
219         if (mPrintStyle == STYLE_INDENTED && safeToIndent) mPrintStream.println();           
220      }  
221      else if (mPrintStyle == STYLE_INDENTED
222               && mPreviousTagType == START_TAG)
223      {      
224         mPrintStream.println();
225      }
226      
227      mCurrentData = new StringBuffer();
228*/
229      if (mDanglingStartTag != null
230          && mCurrentData.length() == 0
231          && mCurrentComment.length() == 0)
232      {
233         emitStartTag(true);
234         mDanglingStartTag = null;
235      }
236      else
237      {
238         if (mDanglingStartTag != null)
239         {
240            emitStartTag(false);
241            mDanglingStartTag = null;
242         }
243 /*
244         if (mCurrentComment.length() > 0)
245         {
246            emitComment();
247         }
248*/
249         if (mCurrentData.length() > 0)
250         {
251            emitContent();
252         }
253
254         popIndentState();
255
256         // Write the end tag
257         if (mPrintStyle == STYLE_INDENTED)
258         {
259            if (mSafeToIndent)
260            {
261               mWriter.println();
262//               if (mPreviousTagType == END_TAG) mPrintStream.println();
263               mWriter.print(StringUtil.polyChar(' ', mCurrentIndent));
264            }
265            mCurrentIndent -= mIndentSize;
266            mPreviousTagType = END_TAG;
267         }
268
269         mWriter.print(XMLUtil.composeEndTag(raw));
270      }
271   }
272   
273   
274   //---------------------------------------------------------------------------
275   public void characters(char[] ch, int start, int length)
276   {
277//      System.out.print("In characters(): ");
278/*      if (mDanglingStartTag != null)
279      {
280         emitStartTag(false);
281         mDanglingStartTag = null;
282      }
283*/
284
285      if (mCurrentData.length() > 0)
286      {
287         mCurrentData.append(ch, start, length);
288
289         if (mPrintStyle == STYLE_INDENTED
290             && mSafeToIndent == true
291             && mCurrentData.length() > 0)
292         {
293            mSafeToIndent = contentLooksSafeToIndent(mCurrentData.toString());
294         }
295
296         if (mDanglingStartTag != null)
297         {
298            emitStartTag(false);
299            mDanglingStartTag = null;
300         }
301
302         emitContent();
303      }
304      else
305      {
306         mCurrentData.append(ch, start, length);
307
308         if (mPrintStyle == STYLE_INDENTED
309             && mSafeToIndent == true
310             && mCurrentData.length() > 0)
311         {
312            mSafeToIndent = contentLooksSafeToIndent(mCurrentData.toString());
313         }
314
315         if (null == mDanglingStartTag)
316         {
317            emitContent();
318         }
319
320      }
321
322/*
323      String content = new String(ch, start, length);
324
325      boolean safeToIndent = true;
326
327      // Print any tag contents
328      if (content.trim().length() > 0)
329      {
330         if (mPrintStyle == STYLE_INDENTED)
331         {
332                safeToIndent = contentLooksSafeToIndent(content);
333
334            if (safeToIndent)
335            {
336               mPrintStream.println();
337               mPrintStream.print(StringUtil.polyChar(' ', mCurrentIndent + mIndentSize));
338            }
339         }
340
341         mPrintStream.print(content);
342
343//         if (mPrintStyle == STYLE_INDENTED && safeToIndent) mPrintStream.println();
344      }
345      else if (mPrintStyle == STYLE_INDENTED
346               && mPreviousTagType == START_TAG)
347      {
348//         mPrintStream.println();
349      }
350*/
351
352   }
353
354
355
356
357
358   //---------------------------------------------------------------------------
359   public void warning(SAXParseException e)
360   {
361      e.printStackTrace();
362   }
363
364
365   //---------------------------------------------------------------------------
366   public void error(SAXParseException e)
367   {
368      e.printStackTrace();
369   }
370
371
372   //---------------------------------------------------------------------------
373   public void skippedEntity(String name)
374   throws SAXException
375   {
376 //            System.out.println("In skippedEntity(): " + name);
377
378   }
379
380   //---------------------------------------------------------------------------
381   public void unparsedEntityDecl(String name,
382                                  String publicId,
383                                  String systemId,
384                                  String notationName)
385   throws SAXException
386   {
387 //            System.out.println("In unparsedEntityDecl(): " + name);
388
389   }
390
391
392
393   // LexicalHandler methods
394
395   //--------------------------------------------------------------------------
396   public void comment(char[] inChars, int offset, int length)
397   {
398//      System.out.println("comment: '" + new String(inChars, offset, length) + "'");
399
400
401      if (mDanglingStartTag != null)
402      {
403         emitStartTag(false);
404         mDanglingStartTag = null;
405      }
406
407      if (mCurrentData.length() > 0)
408      {
409         emitContent();
410      }
411
412 //     popIndentState();
413
414      mCurrentComment.append(inChars, offset, length);
415      emitComment();
416/*
417      if (mCurrentComment.length() > 0)
418      {
419         mCurrentComment.append(inChars, offset, length);
420
421         if (mPrintStyle == STYLE_INDENTED
422             && mSafeToIndent == true
423             && mCurrentComment.length() > 0)
424         {
425            mSafeToIndent = contentLooksSafeToIndent(mCurrentComment.toString());
426         }
427
428         if (mDanglingStartTag != null)
429         {
430            emitStartTag(false);
431            mDanglingStartTag = null;
432         }
433
434         emitComment();
435      }
436      else
437      {
438         mCurrentComment.append(inChars, offset, length);
439
440         if (mPrintStyle == STYLE_INDENTED
441             && mSafeToIndent == true
442             && mCurrentComment.length() > 0)
443         {
444            mSafeToIndent = contentLooksSafeToIndent(mCurrentComment.toString());
445         }
446
447         if (null == mDanglingStartTag)
448         {
449            emitComment();
450         }
451      }
452*/
453
454   }
455
456   //--------------------------------------------------------------------------
457   public void startCDATA()
458   {
459   }
460
461   //--------------------------------------------------------------------------
462   public void endCDATA()
463   {
464   }
465
466   //--------------------------------------------------------------------------
467   public void startEntity(String name)
468   {
469   }
470
471   //--------------------------------------------------------------------------
472   public void endEntity(String name)
473   {
474   }
475
476   //--------------------------------------------------------------------------
477   public void startDTD(String name, String publicId, String systemId)
478   {
479   }
480
481   //--------------------------------------------------------------------------
482   public void endDTD()
483   {
484   }
485
486
487
488
489
490
491
492
493
494   //---------------------------------------------------------------------------
495   // Tags like the HTML pre tag are sensitive to leading and trailing spaces.
496   private boolean contentLooksSafeToIndent(String inContent)
497   {
498      boolean response = true;
499
500      if (Character.isWhitespace(inContent.charAt(0))
501          || Character.isWhitespace(inContent.charAt(inContent.length() - 1)))
502      {
503         response = false;
504      }
505
506      return response;
507   }
508
509   //---------------------------------------------------------------------------
510   private void emitStartTag(boolean inIsEmptyTag)
511   {
512/*
513      if (mCurrentData.length() > 0)
514      {
515         if (mPrintStyle == STYLE_INDENTED)
516         {
517                safeToIndent = contentLooksSafeToIndent(mCurrentData.toString());
518
519            if (safeToIndent)
520            {
521               mPrintStream.println();
522               mPrintStream.print(StringUtil.polyChar(' ', mCurrentIndent + mIndentSize));
523            }
524         }
525
526         mPrintStream.print(mCurrentData.toString());
527
528//         if (mPrintStyle == STYLE_INDENTED && safeToIndent) mPrintStream.println();
529      }
530 */
531
532      if (mPrintStyle == STYLE_INDENTED)
533      {
534         mCurrentIndent += mIndentSize;
535         if (mSafeToIndentStack.size() == 0
536             || ((Boolean) mSafeToIndentStack.peek()).booleanValue())
537         {
538            mWriter.println();
539            mWriter.print(StringUtil.polyChar(' ', mCurrentIndent));
540         }
541
542         if (inIsEmptyTag)
543         {
544            mCurrentIndent -= mIndentSize;
545//            mPreviousTagType = END_TAG;
546         }
547         else
548         {
549            mPreviousTagType = START_TAG;
550            pushIndentState();
551            mSafeToIndent = true;
552         }
553      }
554
555      mWriter.print(XMLUtil.composeStartTag(mDanglingStartTag.getTagName(),
556                                            mDanglingStartTag.getAttributes(),
557                                            inIsEmptyTag));
558
559   }
560
561   //---------------------------------------------------------------------------
562   private void emitContent()
563   {
564
565      if (mPrintStyle == STYLE_INDENTED)
566      {
567         if (mSafeToIndent)
568         {
569            // Re-evaluate
570            mSafeToIndent = contentLooksSafeToIndent(mCurrentData.toString());
571         }
572
573         if (mSafeToIndent)
574         {
575            mWriter.println();
576            mWriter.print(StringUtil.polyChar(' ', mCurrentIndent + mIndentSize));
577         }
578      }
579
580      mWriter.print(mCurrentData.toString());
581      mCurrentData.setLength(0);
582
583   }
584
585   //---------------------------------------------------------------------------
586   private void emitComment()
587   {
588
589      if (mPrintStyle == STYLE_INDENTED)
590      {
591 /*
592         if (mSafeToIndent)
593         {
594            // Re-evaluate
595            mSafeToIndent = contentLooksSafeToIndent(mCurrentComment.toString());
596         }
597*/
598         mSafeToIndent = true;
599
600         if (mSafeToIndent)
601         {
602            mWriter.println();
603            mWriter.print(StringUtil.polyChar(' ', mCurrentIndent + mIndentSize));
604         }
605      }
606
607      mWriter.print("<!--");
608      mWriter.print(mCurrentComment.toString());
609      mWriter.print("-->");
610
611      mCurrentComment.setLength(0);
612
613   }
614
615   //---------------------------------------------------------------------------
616   private void pushIndentState()
617   {
618      mSafeToIndentStack.push(new Boolean(mSafeToIndent));
619   }
620
621   //---------------------------------------------------------------------------
622   private void popIndentState()
623   {
624      mSafeToIndent = ((Boolean) mSafeToIndentStack.pop()).booleanValue();
625   }
626
627}