View Javadoc
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   *         &lt;xs:complexType name="somewhatNamedPerson"&gt;
58   *             &lt;xs:sequence&gt;
59   *                 &lt;xs:element name="firstName" type="xs:string" nillable="true" minOccurs="0"/&gt;
60   *                 &lt;xs:element name="lastName" type="xs:string"/&gt;
61   *             &lt;/xs:sequence&gt;
62   *             &lt;xs:attribute name="age" type="xs:int" use="required"/&gt;
63   *         &lt;/xs:complexType&gt;
64   *     </code>
65   * </pre>
66   * <p>... would be converted to the following annotated XSD, given a DefaultJavaDocRenderer:</p>
67   * <pre>
68   *     <code>
69   *         &lt;xs:complexType name="somewhatNamedPerson"&gt;
70   *             &lt;xs:annotation&gt;
71   *                 &lt;xs:documentation&gt;&lt;![CDATA[Definition of a person with lastName and age, and optionally a firstName as well...
72   *
73   *                 (author): &lt;a href="mailto:lj@jguru.se"&gt;Lennart J&ouml;relid&lt;/a&gt;, jGuru Europe AB
74   *                 (custom): A custom JavaDoc annotation.]]&gt;&lt;/xs:documentation&gt;
75   *             &lt;/xs:annotation&gt;
76   *             &lt;xs:sequence&gt;
77   *                 &lt;xs:element minOccurs="0" name="firstName" nillable="true" type="xs:string"&gt;
78   *                     &lt;xs:annotation&gt;
79   *                         &lt;xs:documentation&gt;&lt;![CDATA[The first name of the SomewhatNamedPerson.]]&gt;&lt;/xs:documentation&gt;
80   *                     &lt;/xs:annotation&gt;
81   *                 &lt;/xs:element&gt;
82   *                 &lt;xs:element name="lastName" type="xs:string"&gt;
83   *                     &lt;xs:annotation&gt;
84   *                         &lt;xs:documentation&gt;&lt;![CDATA[The last name of the SomewhatNamedPerson.]]&gt;&lt;/xs:documentation&gt;
85   *                     &lt;/xs:annotation&gt;
86   *                 &lt;/xs:element&gt;
87   *            &lt;/xs:sequence&gt;
88   *            &lt;xs:attribute name="age" type="xs:int" use="required"&gt;
89   *                &lt;xs:annotation&gt;
90   *                    &lt;xs:documentation&gt;&lt;![CDATA[The age of the SomewhatNamedPerson. Must be positive.]]&gt;&lt;/xs:documentation&gt;
91   *                &lt;/xs:annotation&gt;
92   *            &lt;/xs:attribute&gt;
93   *          &lt;/xs:complexType&gt;
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&ouml;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 }