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 // Append the JavaDoc data Nodes, on the form
231 /*
232 <xs:annotation>
233 <xs:documentation>(JavaDoc here, within a CDATA section)</xs:documentation>
234 </xs:annotation>
235
236 where the "xs" namespace prefix maps to "http://www.w3.org/2001/XMLSchema"
237 */
238 final String standardXsPrefix = "xs";
239 final Document doc = aNode.getOwnerDocument();
240 final Element annotation = doc.createElementNS(XMLConstants.W3C_XML_SCHEMA_NS_URI, ANNOTATION_ELEMENT_NAME);
241 final Element docElement = doc.createElementNS(XMLConstants.W3C_XML_SCHEMA_NS_URI, DOCUMENTATION_ELEMENT_NAME);
242 final CDATASection xsdDocumentation = doc.createCDATASection(renderer.render(javaDocData, location).trim());
243
244 annotation.setPrefix(standardXsPrefix);
245 docElement.setPrefix(standardXsPrefix);
246
247 annotation.appendChild(docElement);
248 final Node firstChildOfCurrentNode = aNode.getFirstChild();
249 if (firstChildOfCurrentNode == null) {
250 aNode.appendChild(annotation);
251 } else {
252 aNode.insertBefore(annotation, firstChildOfCurrentNode);
253 }
254
255 docElement.appendChild(xsdDocumentation);
256 }
257
258 //
259 // Private helpers
260 //
261
262 private static MethodLocation getMethodLocation(final Node aNode, final Set<MethodLocation> methodLocations) {
263
264 MethodLocation toReturn = null;
265
266 if (aNode != null && FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
267
268 final MethodLocation validLocation = getFieldOrMethodLocationIfValid(aNode,
269 getContainingClassOrNull(aNode),
270 methodLocations);
271
272 // The MethodLocation should represent a normal getter; no arguments should be present.
273 if (validLocation != null
274 && MethodLocation.NO_PARAMETERS.equalsIgnoreCase(validLocation.getParametersAsString())) {
275 toReturn = validLocation;
276 }
277 }
278
279 // All done.
280 return toReturn;
281 }
282
283 private static FieldLocation getFieldLocation(final Node aNode, final Set<FieldLocation> fieldLocations) {
284
285 FieldLocation toReturn = null;
286
287 if (aNode != null && FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
288 toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations);
289 }
290
291 // All done.
292 return toReturn;
293 }
294
295 private static <T extends FieldLocation> T getFieldOrMethodLocationIfValid(
296 final Node aNode,
297 final Node containingClassNode,
298 final Set<? extends FieldLocation> locations) {
299
300 T toReturn = null;
301
302 if (containingClassNode != null) {
303
304 // Do we have a FieldLocation corresponding to the supplied Node?
305 for (FieldLocation current : locations) {
306
307 // Validate that the field and class names match the FieldLocation's corresponding values.
308 // Note that we cannot match package names here, as the generated XSD does not contain package
309 // information directly. Instead, we must get the Namespace for the generated Class, and compare
310 // it to the effective Namespace of the current Node.
311 //
312 // However, this is a computational-expensive operation, implying we would rather
313 // do it at processing time when the number of nodes are (considerably?) reduced.
314
315 final String fieldName = current.getMemberName();
316 final String className = current.getClassName();
317
318 try {
319 if (fieldName.equalsIgnoreCase(getName(aNode))
320 && className.equalsIgnoreCase(getName(containingClassNode))) {
321 toReturn = (T) current;
322 }
323 } catch (Exception e) {
324 throw new IllegalStateException("Could not acquire FieldLocation for fieldName ["
325 + fieldName + "] and className [" + className + "]", e);
326 }
327 }
328 }
329
330 // All done.
331 return toReturn;
332 }
333
334 private static ClassLocation getClassLocation(final Node aNode, final Set<ClassLocation> classLocations) {
335
336 if (aNode != null && "complexType".equalsIgnoreCase(aNode.getLocalName())) {
337
338 final String nodeClassName = getName(aNode);
339 for (ClassLocation current : classLocations) {
340
341 // TODO: Ensure that the namespace of the supplied aNode matches the expected namespace.
342 if (current.getClassName().equalsIgnoreCase(nodeClassName)) {
343 return current;
344 }
345 }
346 }
347
348 // Nothing found
349 return null;
350 }
351
352 private static String getName(final Node aNode) {
353
354 final NamedNodeMap attributes = aNode.getAttributes();
355 if (attributes != null) {
356
357 final Node nameNode = attributes.getNamedItem("name");
358 if (nameNode != null) {
359 return nameNode.getNodeValue().trim();
360 }
361 }
362
363 // No name found
364 return null;
365 }
366
367 private static Node getContainingClassOrNull(final Node aNode) {
368
369 for (Node current = aNode.getParentNode(); current != null; current = current.getParentNode()) {
370
371 final String localName = current.getLocalName();
372 if ("complexType".equalsIgnoreCase(localName)) {
373 return current;
374 }
375 }
376
377 // No parent Node found.
378 return null;
379 }
380
381 private static String getXPathFor(final Node aNode) {
382
383 List<String> nodeNameList = new ArrayList<String>();
384
385 for (Node current = aNode; current != null; current = current.getParentNode()) {
386 nodeNameList.add(current.getNodeName() + "[@name='" + getName(current) + "]");
387 }
388
389 StringBuilder builder = new StringBuilder();
390 for (ListIterator<String> it = nodeNameList.listIterator(nodeNameList.size()); it.hasPrevious(); ) {
391 builder.append(it.previous());
392 if (it.hasPrevious()) {
393 builder.append("/");
394 }
395 }
396
397 return builder.toString();
398 }
399 }