1 package org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.NodeProcessor;
23 import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.ClassLocation;
24 import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.FieldLocation;
25 import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.MethodLocation;
26 import org.codehaus.mojo.jaxb2.shared.Validate;
27 import org.w3c.dom.CDATASection;
28 import org.w3c.dom.Document;
29 import org.w3c.dom.Element;
30 import org.w3c.dom.NamedNodeMap;
31 import org.w3c.dom.Node;
32
33 import javax.xml.XMLConstants;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.List;
37 import java.util.ListIterator;
38 import java.util.Set;
39 import java.util.SortedMap;
40
41 /**
42 * <p>Node processor that injects XSD documentation annotations consisting of JavaDoc harvested Java source code
43 * into ComplexTypes, Elements and Attributes. The documentation is injected as follows:</p>
44 * <ol>
45 * <li><strong>ComplexType</strong>: Class-level JavaDoc from the corresponding type is injected as an
46 * annotation directly inside the complexType.</li>
47 * <li><strong>Element</strong>: Field-level JavaDoc (or getter Method-level JavaDoc, in case the Field does
48 * not contain a JavaDoc annotation) from the corresponding member is injected as an
49 * annotation directly inside the element.</li>
50 * <li><strong>Attribute</strong>: Field-level JavaDoc (or getter Method-level JavaDoc, in case the Field does
51 * not contain a JavaDoc annotation) from the corresponding member is injected as an
52 * annotation directly inside the element.</li>
53 * </ol>
54 * <p>Thus, the following 'vanilla'-generated XSD:</p>
55 * <pre>
56 * <code>
57 * <xs:complexType name="somewhatNamedPerson">
58 * <xs:sequence>
59 * <xs:element name="firstName" type="xs:string" nillable="true" minOccurs="0"/>
60 * <xs:element name="lastName" type="xs:string"/>
61 * </xs:sequence>
62 * <xs:attribute name="age" type="xs:int" use="required"/>
63 * </xs:complexType>
64 * </code>
65 * </pre>
66 * <p>... would be converted to the following annotated XSD, given a DefaultJavaDocRenderer:</p>
67 * <pre>
68 * <code>
69 * <xs:complexType name="somewhatNamedPerson">
70 * <xs:annotation>
71 * <xs:documentation><![CDATA[Definition of a person with lastName and age, and optionally a firstName as well...
72 *
73 * (author): <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
74 * (custom): A custom JavaDoc annotation.]]></xs:documentation>
75 * </xs:annotation>
76 * <xs:sequence>
77 * <xs:element minOccurs="0" name="firstName" nillable="true" type="xs:string">
78 * <xs:annotation>
79 * <xs:documentation><![CDATA[The first name of the SomewhatNamedPerson.]]></xs:documentation>
80 * </xs:annotation>
81 * </xs:element>
82 * <xs:element name="lastName" type="xs:string">
83 * <xs:annotation>
84 * <xs:documentation><![CDATA[The last name of the SomewhatNamedPerson.]]></xs:documentation>
85 * </xs:annotation>
86 * </xs:element>
87 * </xs:sequence>
88 * <xs:attribute name="age" type="xs:int" use="required">
89 * <xs:annotation>
90 * <xs:documentation><![CDATA[The age of the SomewhatNamedPerson. Must be positive.]]></xs:documentation>
91 * </xs:annotation>
92 * </xs:attribute>
93 * </xs:complexType>
94 * </code>
95 * </pre>
96 * <p>... given that the Java class <code>SomewhatNamedPerson</code> has JavaDoc on its class and fields
97 * corresponding to the injected XSD annotation/documentation elements.</p>
98 *
99 * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
100 * @see org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocRenderer
101 * @since 2.0
102 */
103 public class XsdAnnotationProcessor implements NodeProcessor {
104
105 /**
106 * The namespace schema prefix for the URI {@code http://www.w3.org/2001/XMLSchema}
107 * (i.e. {@code XMLConstants.W3C_XML_SCHEMA_NS_URI}).
108 *
109 * @see javax.xml.XMLConstants#W3C_XML_SCHEMA_NS_URI
110 */
111 public static final String XSD_SCHEMA_NAMESPACE_PREFIX = "xs";
112
113 /**
114 * The name of the annotation element.
115 */
116 public static final String ANNOTATION_ELEMENT_NAME = "annotation";
117
118 /**
119 * The name of the documentation element.
120 */
121 public static final String DOCUMENTATION_ELEMENT_NAME = "documentation";
122
123 // Internal state
124 private static final List<String> FIELD_METHOD_ELEMENT_NAMES = Arrays.<String>asList("element", "attribute");
125 private SortedMap<ClassLocation, JavaDocData> classJavaDocs;
126 private SortedMap<FieldLocation, JavaDocData> fieldJavaDocs;
127 private SortedMap<MethodLocation, JavaDocData> methodJavaDocs;
128 private JavaDocRenderer renderer;
129
130 /**
131 * Creates an XsdAnnotationProcessor that uses the supplied/generated SearchableDocumentation to read all
132 * JavaDoc structures and the supplied JavaDocRenderer to render JavaDocs into XSD documentation annotations.
133 *
134 * @param docs A non-null SearchableDocumentation, produced from the source code of the JAXB compilation unit.
135 * @param renderer A non-null JavaDocRenderer, used to render the JavaDocData within the SearchableDocumentation.
136 */
137 public XsdAnnotationProcessor(final SearchableDocumentation docs, final JavaDocRenderer renderer) {
138
139 // Check sanity
140 Validate.notNull(docs, "docs");
141 Validate.notNull(renderer, "renderer");
142
143 // Assign internal state
144 this.classJavaDocs = docs.getAll(ClassLocation.class);
145 this.fieldJavaDocs = docs.getAll(FieldLocation.class);
146 this.methodJavaDocs = docs.getAll(MethodLocation.class);
147 this.renderer = renderer;
148 }
149
150 /**
151 * {@inheritDoc}
152 */
153 @Override
154 public boolean accept(final Node aNode) {
155
156 // Only deal with Element nodes.
157 if (aNode.getNodeType() != Node.ELEMENT_NODE || getName(aNode) == null) {
158 return false;
159 }
160
161 /*
162 <xs:complexType name="somewhatNamedPerson">
163 <!-- ClassLocation JavaDocData insertion point -->
164
165 <xs:sequence>
166
167 <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
168 <xs:element name="firstName" type="xs:string" nillable="true" minOccurs="0"/>
169
170 <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
171 <xs:element name="lastName" type="xs:string"/>
172 </xs:sequence>
173
174 <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
175 <xs:attribute name="age" type="xs:int" use="required"/>
176 </xs:complexType>
177 */
178
179 // Only process nodes corresponding to Types we have any JavaDoc for.
180 // TODO: How should we handle PackageLocations and package documentation.
181 boolean toReturn = false;
182 if (getMethodLocation(aNode, methodJavaDocs.keySet()) != null) {
183 toReturn = true;
184 } else if (getFieldLocation(aNode, fieldJavaDocs.keySet()) != null) {
185 toReturn = true;
186 } else if (getClassLocation(aNode, classJavaDocs.keySet()) != null) {
187 toReturn = true;
188 }
189
190 // All done.
191 return toReturn;
192 }
193
194 /**
195 * {@inheritDoc}
196 */
197 @Override
198 public void process(final Node aNode) {
199
200 JavaDocData javaDocData = null;
201 SortableLocation location = null;
202
203 // Insert the documentation annotation into the current Node.
204 final ClassLocation classLocation = getClassLocation(aNode, classJavaDocs.keySet());
205 if (classLocation != null) {
206 javaDocData = classJavaDocs.get(classLocation);
207 location = classLocation;
208 } else {
209
210 final FieldLocation fieldLocation = getFieldLocation(aNode, fieldJavaDocs.keySet());
211 if (fieldLocation != null) {
212 javaDocData = fieldJavaDocs.get(fieldLocation);
213 location = fieldLocation;
214 } else {
215
216 final MethodLocation methodLocation = getMethodLocation(aNode, methodJavaDocs.keySet());
217 if (methodLocation != null) {
218 javaDocData = methodJavaDocs.get(methodLocation);
219 location = methodLocation;
220 }
221 }
222 }
223
224 // We should have a JavaDocData here.
225 if (javaDocData == null) {
226 throw new IllegalStateException("Could not find JavaDocData for XSD node [" + getName(aNode)
227 + "] with XPath [" + getXPathFor(aNode) + "]");
228 }
229
230 //
231 // 1. Append the JavaDoc data Nodes, on the form below
232 // 2. Append the JavaDoc data Nodes only if the renderer yields a non-null/non-empty javadoc.
233 //
234 /*
235 <xs:annotation>
236 <xs:documentation>(JavaDoc here, within a CDATA section)</xs:documentation>
237 </xs:annotation>
238
239 where the "xs" namespace prefix maps to "http://www.w3.org/2001/XMLSchema"
240 */
241 final String processedJavaDoc = renderer.render(javaDocData, location).trim();
242 if (!processedJavaDoc.isEmpty()) {
243
244 final String standardXsPrefix = "xs";
245 final Document doc = aNode.getOwnerDocument();
246 final Element annotation = doc.createElementNS(XMLConstants.W3C_XML_SCHEMA_NS_URI, ANNOTATION_ELEMENT_NAME);
247 final Element docElement = doc.createElementNS(XMLConstants.W3C_XML_SCHEMA_NS_URI, DOCUMENTATION_ELEMENT_NAME);
248 final CDATASection xsdDocumentation = doc.createCDATASection(renderer.render(javaDocData, location).trim());
249
250 annotation.setPrefix(standardXsPrefix);
251 docElement.setPrefix(standardXsPrefix);
252
253 annotation.appendChild(docElement);
254 final Node firstChildOfCurrentNode = aNode.getFirstChild();
255 if (firstChildOfCurrentNode == null) {
256 aNode.appendChild(annotation);
257 } else {
258 aNode.insertBefore(annotation, firstChildOfCurrentNode);
259 }
260
261 docElement.appendChild(xsdDocumentation);
262 }
263 }
264
265 //
266 // Private helpers
267 //
268
269 private static MethodLocation getMethodLocation(final Node aNode, final Set<MethodLocation> methodLocations) {
270
271 MethodLocation toReturn = null;
272
273 if (aNode != null && FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
274
275 final MethodLocation validLocation = getFieldOrMethodLocationIfValid(aNode,
276 getContainingClassOrNull(aNode),
277 methodLocations);
278
279 // The MethodLocation should represent a normal getter; no arguments should be present.
280 if (validLocation != null
281 && MethodLocation.NO_PARAMETERS.equalsIgnoreCase(validLocation.getParametersAsString())) {
282 toReturn = validLocation;
283 }
284 }
285
286 // All done.
287 return toReturn;
288 }
289
290 private static FieldLocation getFieldLocation(final Node aNode, final Set<FieldLocation> fieldLocations) {
291
292 FieldLocation toReturn = null;
293
294 if (aNode != null && FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
295 toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations);
296 }
297
298 // All done.
299 return toReturn;
300 }
301
302 private static <T extends FieldLocation> T getFieldOrMethodLocationIfValid(
303 final Node aNode,
304 final Node containingClassNode,
305 final Set<? extends FieldLocation> locations) {
306
307 T toReturn = null;
308
309 if (containingClassNode != null) {
310
311 // Do we have a FieldLocation corresponding to the supplied Node?
312 for (FieldLocation current : locations) {
313
314 // Validate that the field and class names match the FieldLocation's corresponding values.
315 // Note that we cannot match package names here, as the generated XSD does not contain package
316 // information directly. Instead, we must get the Namespace for the generated Class, and compare
317 // it to the effective Namespace of the current Node.
318 //
319 // However, this is a computational-expensive operation, implying we would rather
320 // do it at processing time when the number of nodes are (considerably?) reduced.
321
322 final String fieldName = current.getMemberName();
323 final String className = current.getClassName();
324
325 try {
326 if (fieldName.equalsIgnoreCase(getName(aNode))
327 && className.equalsIgnoreCase(getName(containingClassNode))) {
328 toReturn = (T) current;
329 }
330 } catch (Exception e) {
331 throw new IllegalStateException("Could not acquire FieldLocation for fieldName ["
332 + fieldName + "] and className [" + className + "]", e);
333 }
334 }
335 }
336
337 // All done.
338 return toReturn;
339 }
340
341 private static ClassLocation getClassLocation(final Node aNode, final Set<ClassLocation> classLocations) {
342
343 if (aNode != null && "complexType".equalsIgnoreCase(aNode.getLocalName())) {
344
345 final String nodeClassName = getName(aNode);
346 for (ClassLocation current : classLocations) {
347
348 // TODO: Ensure that the namespace of the supplied aNode matches the expected namespace.
349 if (current.getClassName().equalsIgnoreCase(nodeClassName)) {
350 return current;
351 }
352 }
353 }
354
355 // Nothing found
356 return null;
357 }
358
359 private static String getName(final Node aNode) {
360
361 final NamedNodeMap attributes = aNode.getAttributes();
362 if (attributes != null) {
363
364 final Node nameNode = attributes.getNamedItem("name");
365 if (nameNode != null) {
366 return nameNode.getNodeValue().trim();
367 }
368 }
369
370 // No name found
371 return null;
372 }
373
374 private static Node getContainingClassOrNull(final Node aNode) {
375
376 for (Node current = aNode.getParentNode(); current != null; current = current.getParentNode()) {
377
378 final String localName = current.getLocalName();
379 if ("complexType".equalsIgnoreCase(localName)) {
380 return current;
381 }
382 }
383
384 // No parent Node found.
385 return null;
386 }
387
388 private static String getXPathFor(final Node aNode) {
389
390 List<String> nodeNameList = new ArrayList<String>();
391
392 for (Node current = aNode; current != null; current = current.getParentNode()) {
393 nodeNameList.add(current.getNodeName() + "[@name='" + getName(current) + "]");
394 }
395
396 StringBuilder builder = new StringBuilder();
397 for (ListIterator<String> it = nodeNameList.listIterator(nodeNameList.size()); it.hasPrevious(); ) {
398 builder.append(it.previous());
399 if (it.hasPrevious()) {
400 builder.append("/");
401 }
402 }
403
404 return builder.toString();
405 }
406 }