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