View Javadoc

1   package org.codehaus.modello.plugin.xdoc;
2   
3   /*
4    * Copyright (c) 2004, Codehaus.org
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy of
7    * this software and associated documentation files (the "Software"), to deal in
8    * the Software without restriction, including without limitation the rights to
9    * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10   * of the Software, and to permit persons to whom the Software is furnished to do
11   * so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in all
14   * copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22   * SOFTWARE.
23   */
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.Writer;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Properties;
31  import java.util.Set;
32  import java.util.Stack;
33  
34  import org.codehaus.modello.ModelloException;
35  import org.codehaus.modello.ModelloParameterConstants;
36  import org.codehaus.modello.ModelloRuntimeException;
37  import org.codehaus.modello.model.BaseElement;
38  import org.codehaus.modello.model.Model;
39  import org.codehaus.modello.model.ModelAssociation;
40  import org.codehaus.modello.model.ModelClass;
41  import org.codehaus.modello.model.ModelDefault;
42  import org.codehaus.modello.model.ModelField;
43  import org.codehaus.modello.model.Version;
44  import org.codehaus.modello.model.VersionRange;
45  import org.codehaus.modello.plugin.xdoc.metadata.XdocFieldMetadata;
46  import org.codehaus.modello.plugin.xsd.XsdModelHelper;
47  import org.codehaus.modello.plugins.xml.AbstractXmlGenerator;
48  import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
49  import org.codehaus.modello.plugins.xml.metadata.XmlClassMetadata;
50  import org.codehaus.modello.plugins.xml.metadata.XmlFieldMetadata;
51  import org.codehaus.modello.plugins.xml.metadata.XmlModelMetadata;
52  import org.codehaus.plexus.util.StringUtils;
53  import org.codehaus.plexus.util.WriterFactory;
54  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
55  import org.codehaus.plexus.util.xml.XMLWriter;
56  import org.jsoup.Jsoup;
57  import org.jsoup.nodes.Document;
58  
59  /**
60   * @author <a href="mailto:jason@modello.org">Jason van Zyl</a>
61   * @author <a href="mailto:emmanuel@venisse.net">Emmanuel Venisse</a>
62   */
63  public class XdocGenerator
64      extends AbstractXmlGenerator
65  {
66      private static final VersionRange DEFAULT_VERSION_RANGE = new VersionRange( "0.0.0+" );
67  
68      private Version firstVersion = DEFAULT_VERSION_RANGE.getFromVersion();
69  
70      private Version version = DEFAULT_VERSION_RANGE.getFromVersion();
71  
72      public void generate( Model model, Properties parameters )
73          throws ModelloException
74      {
75          initialize( model, parameters );
76  
77          if ( parameters.getProperty( ModelloParameterConstants.FIRST_VERSION ) != null )
78          {
79              firstVersion = new Version( parameters.getProperty( ModelloParameterConstants.FIRST_VERSION ) );
80          }
81  
82          if ( parameters.getProperty( ModelloParameterConstants.VERSION ) != null )
83          {
84              version = new Version( parameters.getProperty( ModelloParameterConstants.VERSION ) );
85          }
86  
87          try
88          {
89              generateXdoc( parameters );
90          }
91          catch ( IOException ex )
92          {
93              throw new ModelloException( "Exception while generating XDoc.", ex );
94          }
95      }
96  
97      private void generateXdoc( Properties parameters )
98          throws IOException
99      {
100         Model objectModel = getModel();
101 
102         File directory = getOutputDirectory();
103 
104         if ( isPackageWithVersion() )
105         {
106             directory = new File( directory, getGeneratedVersion().toString() );
107         }
108 
109         if ( !directory.exists() )
110         {
111             directory.mkdirs();
112         }
113 
114         // we assume parameters not null
115         String xdocFileName = parameters.getProperty( ModelloParameterConstants.OUTPUT_XDOC_FILE_NAME );
116 
117         File f = new File( directory, objectModel.getId() + ".xml" );
118 
119         if ( xdocFileName != null )
120         {
121             f = new File( directory, xdocFileName );
122         }
123 
124         Writer writer = WriterFactory.newXmlWriter( f );
125 
126         XMLWriter w = new PrettyPrintXMLWriter( writer );
127 
128         writer.write( "<?xml version=\"1.0\"?>\n" );
129 
130         initHeader( w );
131 
132         w.startElement( "document" );
133 
134         w.startElement( "properties" );
135 
136         writeTextElement( w, "title", objectModel.getName() );
137 
138         w.endElement();
139 
140         // Body
141 
142         w.startElement( "body" );
143 
144         w.startElement( "section" );
145 
146         w.addAttribute( "name", objectModel.getName() );
147 
148         writeMarkupElement( w, "p", getDescription( objectModel ) );
149 
150         // XML representation of the model with links
151         ModelClass root = objectModel.getClass( objectModel.getRoot( getGeneratedVersion() ), getGeneratedVersion() );
152 
153         writeMarkupElement( w, "source", "\n" + getModelXmlDescriptor( root ) );
154 
155         // Element descriptors
156         // Traverse from root so "abstract" models aren't included
157         writeModelDescriptor( w, root );
158 
159         w.endElement();
160 
161         w.endElement();
162 
163         w.endElement();
164 
165         writer.flush();
166 
167         writer.close();
168     }
169 
170     /**
171      * Get the anchor name by which model classes can be accessed in the generated xdoc/html file.
172      *
173      * @param tagName the name of the XML tag of the model class
174      * @return the corresponding anchor name
175      */
176     private String getAnchorName( String tagName )
177     {
178         return "class_" + tagName ;
179     }
180 
181     /**
182      * Write description of the whole model.
183      *
184      * @param w the output writer
185      * @param rootModelClass the root class of the model
186      */
187     private void writeModelDescriptor( XMLWriter w, ModelClass rootModelClass )
188     {
189         writeElementDescriptor( w, rootModelClass, null, new HashSet<String>() );
190     }
191 
192     /**
193      * Write description of an element of the XML representation of the model. This method is recursive.
194      *
195      * @param w the output writer
196      * @param modelClass the mode class to describe
197      * @param association the association we are coming from (can be <code>null</code>)
198      * @param written set of data already written
199      */
200     private void writeElementDescriptor( XMLWriter w, ModelClass modelClass, ModelAssociation association,
201                                          Set<String> written )
202     {
203         String tagName = resolveTagName( modelClass, association );
204 
205         String id = getId( tagName, modelClass );
206         if ( written.contains( id ) )
207         {
208             // tag already written for this model class accessed as this tag name
209             return;
210         }
211         written.add( id );
212 
213         written.add( tagName );
214 
215         w.startElement( "a" );
216 
217         w.addAttribute( "name", getAnchorName( tagName ) );
218 
219         w.endElement();
220 
221         w.startElement( "subsection" );
222 
223         w.addAttribute( "name", tagName );
224 
225         writeMarkupElement( w, "p", getDescription( modelClass ) );
226 
227         List<ModelField> elementFields = getFieldsForXml( modelClass, getGeneratedVersion() );
228 
229         ModelField contentField = getContentField( elementFields );
230 
231         if ( contentField != null )
232         {
233             // this model class has a Content field
234             w.startElement( "p" );
235 
236             writeTextElement( w, "b", "Element Content: " );
237 
238             w.writeMarkup( getDescription( contentField ) );
239 
240             w.endElement();
241         }
242 
243         List<ModelField> attributeFields = getXmlAttributeFields( elementFields );
244 
245         elementFields.removeAll( attributeFields );
246 
247         writeFieldsTable( w, attributeFields, false ); // write attributes
248         writeFieldsTable( w, elementFields, true ); // write elements
249 
250         w.endElement();
251 
252         // check every fields that are inner associations to write their element descriptor
253         for ( ModelField f : elementFields )
254         {
255             if ( isInnerAssociation( f ) )
256             {
257                 ModelAssociation assoc = (ModelAssociation) f;
258                 ModelClass fieldModelClass = getModel().getClass( assoc.getTo(), getGeneratedVersion() );
259 
260                 if ( !written.contains( getId( resolveTagName( fieldModelClass, assoc ), fieldModelClass ) ) )
261                 {
262                     writeElementDescriptor( w, fieldModelClass, assoc, written );
263                 }
264             }
265         }
266     }
267 
268     private String getId( String tagName, ModelClass modelClass )
269     {
270         return tagName + '/' + modelClass.getPackageName() + '.' + modelClass.getName();
271     }
272 
273     /**
274      * Write a table containing model fields description.
275      *
276      * @param w the output writer
277      * @param fields the fields to add in the table
278      * @param elementFields <code>true</code> if fields are elements, <code>false</code> if fields are attributes
279      */
280     private void writeFieldsTable( XMLWriter w, List<ModelField> fields, boolean elementFields )
281     {
282         if ( fields == null || fields.isEmpty() )
283         {
284             // skip empty table
285             return;
286         }
287 
288         // skip if only one element field with xml.content == true
289         if ( elementFields && ( fields.size() == 1 ) && hasContentField( fields ) )
290         {
291             return;
292         }
293 
294         w.startElement( "table" );
295 
296         w.startElement( "tr" );
297 
298         writeTextElement( w, "th", elementFields ? "Element" : "Attribute" );
299 
300         writeTextElement( w, "th", "Type" );
301 
302         boolean showSinceColumn = version.greaterThan( firstVersion );
303 
304         if ( showSinceColumn )
305         {
306             writeTextElement( w, "th", "Since" );
307         }
308 
309         writeTextElement( w, "th", "Description" );
310 
311         w.endElement(); // tr
312 
313         for ( ModelField f : fields )
314         {
315             XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) f.getMetadata( XmlFieldMetadata.ID );
316 
317             if ( xmlFieldMetadata.isContent() )
318             {
319                 continue;
320             }
321 
322             w.startElement( "tr" );
323 
324             // Element/Attribute column
325 
326             String tagName = resolveTagName( f, xmlFieldMetadata );
327 
328             w.startElement( "td" );
329 
330             w.startElement( "code" );
331 
332             boolean manyAssociation = false;
333 
334             if ( f instanceof ModelAssociation )
335             {
336                 ModelAssociation assoc = (ModelAssociation) f;
337 
338                 XmlAssociationMetadata xmlAssociationMetadata =
339                     (XmlAssociationMetadata) assoc.getAssociationMetadata( XmlAssociationMetadata.ID );
340 
341                 manyAssociation = assoc.isManyMultiplicity();
342 
343                 String itemTagName = manyAssociation ? resolveTagName( tagName, xmlAssociationMetadata ) : tagName;
344 
345                 if ( manyAssociation && xmlAssociationMetadata.isWrappedItems() )
346                 {
347                     w.writeText( tagName );
348                     w.writeMarkup( "/" );
349                 }
350                 if ( isInnerAssociation( f ) )
351                 {
352                     w.startElement( "a" );
353                     w.addAttribute( "href", "#" + getAnchorName( itemTagName ) );
354                     w.writeText( itemTagName );
355                     w.endElement();
356                 }
357                 else if ( ModelDefault.PROPERTIES.equals( f.getType() ) )
358                 {
359                     if ( xmlAssociationMetadata.isMapExplode() )
360                     {
361                         w.writeText( "(key,value)" );
362                     }
363                     else
364                     {
365                         w.writeMarkup( "<i>key</i>=<i>value</i>" );
366                     }
367                 }
368                 else
369                 {
370                     w.writeText( itemTagName );
371                 }
372                 if ( manyAssociation )
373                 {
374                     w.writeText( "*" );
375                 }
376             }
377             else
378             {
379                 w.writeText( tagName );
380             }
381 
382             w.endElement(); // code
383 
384             w.endElement(); // td
385 
386             // Type column
387 
388             w.startElement( "td" );
389 
390             w.startElement( "code" );
391 
392             if ( f instanceof ModelAssociation )
393             {
394                 ModelAssociation assoc = (ModelAssociation) f;
395 
396                 if ( assoc.isOneMultiplicity() )
397                 {
398                     w.writeText( assoc.getTo() );
399                 }
400                 else
401                 {
402                     w.writeText( assoc.getType().substring( "java.util.".length() ) );
403 
404                     if ( assoc.isGenericType() )
405                     {
406                         w.writeText( "<" + assoc.getTo() + ">" );
407                     }
408                 }
409             }
410             else
411             {
412                 w.writeText( f.getType() );
413             }
414 
415             w.endElement(); // code
416 
417             w.endElement(); // td
418 
419             // Since column
420 
421             if ( showSinceColumn )
422             {
423                 w.startElement( "td" );
424 
425                 if ( f.getVersionRange() != null )
426                 {
427                     Version fromVersion = f.getVersionRange().getFromVersion();
428                     if ( fromVersion != null && fromVersion.greaterThan( firstVersion ) )
429                     {
430                         w.writeMarkup( fromVersion.toString() );
431                     }
432                 }
433 
434                 w.endElement();
435             }
436 
437             // Description column
438 
439             w.startElement( "td" );
440 
441             if ( manyAssociation )
442             {
443                 w.writeMarkup( "<b>(Many)</b> " );
444             }
445 
446             w.writeMarkup( getDescription( f ) );
447 
448             // Write the default value, if it exists.
449             // But only for fields that are not a ModelAssociation
450             if ( f.getDefaultValue() != null && !( f instanceof ModelAssociation ) )
451             {
452                 w.writeMarkup( "<br/><strong>Default value is</strong>: " );
453 
454                 writeTextElement( w, "code", f.getDefaultValue() );
455 
456                 w.writeText( "." );
457             }
458 
459             w.endElement(); // td
460 
461             w.endElement(); // tr
462         }
463 
464         w.endElement(); // table
465 
466     }
467 
468     /**
469      * Build the pretty tree describing the XML representation of the model.
470      *
471      * @param rootModelClass the model root class
472      * @return the String representing the tree model
473      */
474     private String getModelXmlDescriptor( ModelClass rootModelClass )
475     {
476         return getElementXmlDescriptor( rootModelClass, null, new Stack<String>() );
477     }
478 
479     /**
480      * Build the pretty tree describing the XML representation of an element of the model. This method is recursive.
481      *
482      * @param modelClass the class we are printing the model
483      * @param association the association we are coming from (can be <code>null</code>)
484      * @param stack the stack of elements that have been traversed to come to the current one
485      * @return the String representing the tree model
486      * @throws ModelloRuntimeException
487      */
488     private String getElementXmlDescriptor( ModelClass modelClass, ModelAssociation association, Stack<String> stack )
489         throws ModelloRuntimeException
490     {
491         StringBuffer sb = new StringBuffer();
492 
493         appendSpacer( sb, stack.size() );
494 
495         String tagName = resolveTagName( modelClass, association );
496 
497         // <tagName
498         sb.append( "&lt;<a href=\"#" ).append( getAnchorName( tagName ) ).append( "\">" );
499         sb.append( tagName ).append( "</a>" );
500 
501         boolean addNewline = false;
502         if ( stack.size() == 0 )
503         {
504             // try to add XML Schema reference
505             try
506             {
507                 String targetNamespace = XsdModelHelper.getTargetNamespace( modelClass.getModel(), getGeneratedVersion() );
508 
509                 XmlModelMetadata xmlModelMetadata = (XmlModelMetadata) modelClass.getModel().getMetadata( XmlModelMetadata.ID );
510 
511                 if ( StringUtils.isNotBlank( targetNamespace ) && ( xmlModelMetadata.getSchemaLocation() != null ) )
512                 {
513                     String schemaLocation = xmlModelMetadata.getSchemaLocation( getGeneratedVersion() );
514 
515                     sb.append( " xmlns=\"" + targetNamespace + "\"" );
516                     sb.append( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" );
517                     sb.append( "  xsi:schemaLocation=\"" + targetNamespace );
518                     sb.append( " <a href=\"" + schemaLocation + "\">" + schemaLocation + "</a>\"" );
519 
520                     addNewline = true;
521                 }
522             }
523             catch ( ModelloException me )
524             {
525                 // ignore unavailable XML Schema configuration
526             }
527         }
528 
529         String id = tagName + '/' + modelClass.getPackageName() + '.' + modelClass.getName();
530         if ( stack.contains( id ) )
531         {
532             // recursion detected
533             sb.append( "&gt;...recursion...&lt;" ).append( tagName ).append( "&gt;\n" );
534             return sb.toString();
535         }
536 
537         List<ModelField> fields = getFieldsForXml( modelClass, getGeneratedVersion() );
538 
539         List<ModelField> attributeFields = getXmlAttributeFields( fields );
540 
541         if ( attributeFields.size() > 0 )
542         {
543 
544             for ( ModelField f : attributeFields )
545             {
546                 XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) f.getMetadata( XmlFieldMetadata.ID );
547 
548                 if ( addNewline )
549                 {
550                     addNewline = false;
551 
552                     sb.append( "\n  " );
553                 }
554                 else
555                 {
556                     sb.append( ' ' );
557                 }
558 
559                 sb.append( resolveTagName( f, xmlFieldMetadata ) ).append( "=.." );
560             }
561 
562             sb.append( ' ' );
563 
564         }
565 
566         fields.removeAll( attributeFields );
567 
568         if ( ( fields.size() == 0 ) || ( ( fields.size() == 1 ) && hasContentField( fields ) ) )
569         {
570             sb.append( "/&gt;\n" );
571         }
572         else
573         {
574             sb.append( "&gt;\n" );
575 
576             stack.push( id );
577 
578             for ( ModelField f : fields )
579             {
580                 XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) f.getMetadata( XmlFieldMetadata.ID );
581 
582                 XdocFieldMetadata xdocFieldMetadata = (XdocFieldMetadata) f.getMetadata( XdocFieldMetadata.ID );
583 
584                 if ( XdocFieldMetadata.BLANK.equals( xdocFieldMetadata.getSeparator() ) )
585                 {
586                     sb.append( '\n' );
587                 }
588 
589                 String fieldTagName = resolveTagName( f, xmlFieldMetadata );
590 
591                 if ( isInnerAssociation( f ) )
592                 {
593                     ModelAssociation assoc = (ModelAssociation) f;
594 
595                     boolean wrappedItems = false;
596                     if ( assoc.isManyMultiplicity() )
597                     {
598                         XmlAssociationMetadata xmlAssociationMetadata =
599                             (XmlAssociationMetadata) assoc.getAssociationMetadata( XmlAssociationMetadata.ID );
600                         wrappedItems = xmlAssociationMetadata.isWrappedItems();
601                     }
602 
603                     if ( wrappedItems )
604                     {
605                         appendSpacer( sb, stack.size() );
606 
607                         sb.append( "&lt;" ).append( fieldTagName ).append( "&gt;\n" );
608 
609                         stack.push( fieldTagName );
610                     }
611 
612                     ModelClass fieldModelClass = getModel().getClass( assoc.getTo(), getGeneratedVersion() );
613 
614                     sb.append( getElementXmlDescriptor( fieldModelClass, assoc, stack ) );
615 
616                     if ( wrappedItems )
617                     {
618                         stack.pop();
619 
620                         appendSpacer( sb, stack.size() );
621 
622                         sb.append( "&lt;/" ).append( fieldTagName ).append( "&gt;\n" );
623                     }
624                 }
625                 else if ( ModelDefault.PROPERTIES.equals( f.getType() ) )
626                 {
627                     ModelAssociation assoc = (ModelAssociation) f;
628                     XmlAssociationMetadata xmlAssociationMetadata =
629                         (XmlAssociationMetadata) assoc.getAssociationMetadata( XmlAssociationMetadata.ID );
630 
631                     appendSpacer( sb, stack.size() );
632                     sb.append( "&lt;" ).append( fieldTagName ).append( "&gt;\n" );
633 
634                     if ( xmlAssociationMetadata.isMapExplode() )
635                     {
636                         appendSpacer( sb, stack.size() + 1 );
637                         sb.append( "&lt;key/&gt;\n" );
638                         appendSpacer( sb, stack.size() + 1 );
639                         sb.append( "&lt;value/&gt;\n" );
640                     }
641                     else
642                     {
643                         appendSpacer( sb, stack.size() + 1 );
644                         sb.append( "&lt;<i>key</i>&gt;<i>value</i>&lt;/<i>key</i>&gt;\n" );
645                     }
646 
647                     appendSpacer( sb, stack.size() );
648                     sb.append( "&lt;/" ).append( fieldTagName ).append( "&gt;\n" );
649                 }
650                 else
651                 {
652                     appendSpacer( sb, stack.size() );
653 
654                     sb.append( "&lt;" ).append( fieldTagName ).append( "/&gt;\n" );
655                 }
656             }
657 
658             stack.pop();
659 
660             appendSpacer( sb, stack.size() );
661 
662             sb.append( "&lt;/" ).append( tagName ).append( "&gt;\n" );
663         }
664 
665         return sb.toString();
666     }
667 
668     /**
669      * Compute the tagName of a given class, living inside an association.
670      * @param modelClass the class we are looking for the tag name
671      * @param association the association where this class is used
672      * @return the tag name to use
673      * @todo refactor to use resolveTagName helpers instead
674      */
675     private String resolveTagName( ModelClass modelClass, ModelAssociation association )
676     {
677         XmlClassMetadata xmlClassMetadata = (XmlClassMetadata) modelClass.getMetadata( XmlClassMetadata.ID );
678 
679         String tagName;
680         if ( xmlClassMetadata == null || xmlClassMetadata.getTagName() == null )
681         {
682             if ( association == null )
683             {
684                 tagName = uncapitalise( modelClass.getName() );
685             }
686             else
687             {
688                 tagName = association.getName();
689 
690                 if ( association.isManyMultiplicity() )
691                 {
692                     tagName = singular( tagName );
693                 }
694             }
695         }
696         else
697         {
698             tagName = xmlClassMetadata.getTagName();
699         }
700 
701         if ( association != null )
702         {
703             XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) association.getMetadata( XmlFieldMetadata.ID );
704 
705             XmlAssociationMetadata xmlAssociationMetadata =
706                 (XmlAssociationMetadata) association.getAssociationMetadata( XmlAssociationMetadata.ID );
707 
708             if ( xmlFieldMetadata != null )
709             {
710                 if ( xmlAssociationMetadata.getTagName() != null )
711                 {
712                     tagName = xmlAssociationMetadata.getTagName();
713                 }
714                 else if ( xmlFieldMetadata.getTagName() != null )
715                 {
716                     tagName = xmlFieldMetadata.getTagName();
717 
718                     if ( association.isManyMultiplicity() )
719                     {
720                         tagName = singular( tagName );
721                     }
722                 }
723             }
724         }
725 
726         return tagName;
727     }
728 
729     /**
730      * Appends the required spacers to the given StringBuffer.
731      * @param sb where to append the spacers
732      * @param depth the depth of spacers to generate
733      */
734     private static void appendSpacer( StringBuffer sb, int depth )
735     {
736         for ( int i = 0; i < depth; i++ )
737         {
738             sb.append( "  " );
739         }
740     }
741 
742     private static String getDescription( BaseElement element )
743     {
744         return ( element.getDescription() == null ) ? "No description." : rewrite( element.getDescription() );
745     }
746 
747     private static void writeTextElement( XMLWriter w, String name, String text )
748     {
749         w.startElement( name );
750         w.writeText( text );
751         w.endElement();
752     }
753 
754     private static void writeMarkupElement( XMLWriter w, String name, String markup )
755     {
756         w.startElement( name );
757         w.writeMarkup( markup );
758         w.endElement();
759     }
760     
761     /**
762      * Ensures that text will have balanced tags
763      * 
764      * @param text xml or html based content
765      * @return valid XML string
766      */
767     private static String rewrite( String text )
768     {
769         Document document = Jsoup.parseBodyFragment( text );
770         document.outputSettings().syntax( Document.OutputSettings.Syntax.xml );
771         return document.body().html();
772     }
773 }