1 package org.codehaus.modello.plugin.xdoc;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
67
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
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
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
145 ModelClass root = objectModel.getClass(objectModel.getRoot(getGeneratedVersion()), getGeneratedVersion());
146
147 writeMarkupElement(w, "source", "\n" + getModelXmlDescriptor(root));
148
149
150
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
166
167
168
169
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
181
182
183
184
185 private void writeModelDescriptor(XMLWriter w, ModelClass rootModelClass) {
186 writeElementDescriptor(w, rootModelClass, null, new HashSet<>(), new HashMap<>());
187 }
188
189
190
191
192
193
194
195
196
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
209 return;
210 }
211 writtenIds.add(id);
212
213 String anchorName = getAnchorName(tagName, modelClass);
214 if (writtenAnchors.containsKey(anchorName)) {
215
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
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);
254 writeFieldsTable(w, elementFields, true);
255
256 w.endElement();
257
258
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
277
278
279
280
281
282 private void writeFieldsTable(XMLWriter w, List<ModelField> fields, boolean elementFields) {
283 if (fields == null || fields.isEmpty()) {
284
285 return;
286 }
287
288
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();
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
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();
366
367 w.endElement();
368
369
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();
392
393 w.endElement();
394
395
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
411
412 w.startElement("td");
413
414 if (manyAssociation) {
415 w.writeMarkup("<b>(Many)</b> ");
416 }
417
418 w.writeMarkup(getDescription(f));
419
420
421
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();
431
432 w.endElement();
433 }
434
435 w.endElement();
436 }
437
438
439
440
441
442
443
444 private String getModelXmlDescriptor(ModelClass rootModelClass) {
445 return getElementXmlDescriptor(rootModelClass, null, new Stack<String>());
446 }
447
448
449
450
451
452
453
454
455
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
466 sb.append("<<a href=\"#").append(getAnchorName(tagName, modelClass)).append("\">");
467 sb.append(tagName).append("</a>");
468
469 boolean addNewline = false;
470 if (stack.size() == 0) {
471
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
491 }
492 }
493
494 String id = tagName + '/' + modelClass.getPackageName() + '.' + modelClass.getName();
495 if (stack.contains(id)) {
496
497 sb.append(">...recursion...<").append(tagName).append(">\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("/>\n");
528 } else {
529 sb.append(">\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("<").append(fieldTagName).append(">\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("</").append(fieldTagName).append(">\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("<").append(fieldTagName).append(">\n");
580
581 if (xmlAssociationMetadata.isMapExplode()) {
582 appendSpacer(sb, stack.size() + 1);
583 sb.append("<key/>\n");
584 appendSpacer(sb, stack.size() + 1);
585 sb.append("<value/>\n");
586 } else {
587 appendSpacer(sb, stack.size() + 1);
588 sb.append("<<i>key</i>><i>value</i></<i>key</i>>\n");
589 }
590
591 appendSpacer(sb, stack.size());
592 sb.append("</").append(fieldTagName).append(">\n");
593 } else {
594 appendSpacer(sb, stack.size());
595
596 sb.append("<").append(fieldTagName).append("/>\n");
597 }
598 }
599
600 stack.pop();
601
602 appendSpacer(sb, stack.size());
603
604 sb.append("</").append(tagName).append(">\n");
605 }
606
607 return sb.toString();
608 }
609
610
611
612
613
614
615
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
659
660
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
686
687
688
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 }