View Javadoc
1   package org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc;
2   
3   import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.ClassLocation;
4   import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.FieldLocation;
5   import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.MethodLocation;
6   import org.w3c.dom.CDATASection;
7   import org.w3c.dom.Document;
8   import org.w3c.dom.Element;
9   import org.w3c.dom.NamedNodeMap;
10  import org.w3c.dom.Node;
11  
12  import javax.xml.XMLConstants;
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.Collections;
16  import java.util.List;
17  import java.util.ListIterator;
18  import java.util.Set;
19  import java.util.SortedMap;
20  
21  /**
22   * Helper class stashing commonly used algorithms to work with DOM documents.
23   *
24   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>, jGuru Europe AB
25   * @since 2.3
26   */
27  public final class DomHelper {
28  
29      private static final String NAME_ATTRIBUTE = "name";
30      private static final String VALUE_ATTRIBUTE = "value";
31  
32      /**
33       * The name of the annotation element.
34       */
35      public static final String ANNOTATION_ELEMENT_NAME = "annotation";
36  
37      /**
38       * The name of the documentation element.
39       */
40      public static final String DOCUMENTATION_ELEMENT_NAME = "documentation";
41  
42      /**
43       * The namespace schema prefix for the URI {@code http://www.w3.org/2001/XMLSchema}
44       * (i.e. {@code XMLConstants.W3C_XML_SCHEMA_NS_URI}).
45       *
46       * @see javax.xml.XMLConstants#W3C_XML_SCHEMA_NS_URI
47       */
48      public static final String XSD_SCHEMA_NAMESPACE_PREFIX = "xs";
49  
50      /**
51       * The names of DOM Elements corresponding to Java class Fields or Methods.
52       */
53      public static final List<String> CLASS_FIELD_METHOD_ELEMENT_NAMES = Arrays.asList("element", "attribute");
54  
55      /**
56       * The names of DOM Elements corresponding to Java enum Fields or Methods.
57       */
58      public static final List<String> ENUMERATION_FIELD_METHOD_ELEMENT_NAMES = Collections.singletonList("enumeration");
59  
60      /*
61       * Hide constructor for utility classes
62       */
63      private DomHelper() {
64      }
65  
66      /**
67       * Retrieves the value of the {@code name} attribute of the supplied Node.
68       *
69       * @param aNode A DOM Node.
70       * @return the value of the {@code name} attribute of the supplied Node/Element.
71       */
72      public static String getNameAttribute(final Node aNode) {
73          return getNamedAttribute(aNode, NAME_ATTRIBUTE);
74      }
75  
76      /**
77       * Retrieves the value of the {@code value} attribute of the supplied Node.
78       *
79       * @param aNode A DOM Node.
80       * @return the value of the {@code value} attribute of the supplied Node/Element.
81       */
82      public static String getValueAttribute(final Node aNode) {
83          return getNamedAttribute(aNode, VALUE_ATTRIBUTE);
84      }
85  
86      /**
87       * Checks if the supplied DOM Node is a DOM Element having a defined "name" attribute.
88       *
89       * @param aNode A DOM Node.
90       * @return {@code true} if the supplied aNode is an Elemnet having a defined "name" attribute.
91       */
92      public static boolean isNamedElement(final Node aNode) {
93  
94          final boolean isElementNode = aNode != null && aNode.getNodeType() == Node.ELEMENT_NODE;
95  
96          return isElementNode
97                  && getNamedAttribute(aNode, NAME_ATTRIBUTE) != null
98                  && !getNamedAttribute(aNode, NAME_ATTRIBUTE).isEmpty();
99      }
100 
101     /**
102      * Retrieves the TagName for the supplied Node if it is an Element, and null otherwise.
103      *
104      * @param aNode A DOM Node.
105      * @return The TagName of the Node if it is an Element, and null otherwise.
106      */
107     public static String getElementTagName(final Node aNode) {
108 
109         if (aNode != null && aNode.getNodeType() == Node.ELEMENT_NODE) {
110 
111             final Element theElement = (Element) aNode;
112             return theElement.getTagName();
113         }
114 
115         // The Node was not an Element.
116         return null;
117     }
118 
119     /**
120      * <p>Adds the given formattedDocumentation within an XML documentation annotation under the supplied Node.
121      * Only adds the documentation annotation if the formattedDocumentation is non-null and non-empty. The
122      * documentation annotation is on the form:</p>
123      * <pre>
124      *     <code>
125      *         &lt;xs:annotation&gt;
126      *             &lt;xs:documentation&gt;(JavaDoc here, within a CDATA section)&lt;/xs:documentation&gt;
127      *         &lt;/xs:annotation&gt;
128      *     </code>
129      * </pre>
130      *
131      * @param aNode                  The non-null Node to which an XML documentation annotation should be added.
132      * @param formattedDocumentation The documentation text to add.
133      */
134     public static void addXmlDocumentAnnotationTo(final Node aNode, final String formattedDocumentation) {
135 
136         if (aNode != null && formattedDocumentation != null && !formattedDocumentation.isEmpty()) {
137 
138             // Add the new Elements, as required.
139             final Document doc = aNode.getOwnerDocument();
140             final Element annotation = doc.createElementNS(
141                     XMLConstants.W3C_XML_SCHEMA_NS_URI, ANNOTATION_ELEMENT_NAME);
142             final Element docElement = doc.createElementNS(
143                     XMLConstants.W3C_XML_SCHEMA_NS_URI, DOCUMENTATION_ELEMENT_NAME);
144             final CDATASection xsdDocumentation = doc.createCDATASection(formattedDocumentation);
145 
146             // Set the prefixes
147             annotation.setPrefix(XSD_SCHEMA_NAMESPACE_PREFIX);
148             docElement.setPrefix(XSD_SCHEMA_NAMESPACE_PREFIX);
149 
150             // Inject the formattedDocumentation into the CDATA section.
151             annotation.appendChild(docElement);
152             final Node firstChildOfCurrentNode = aNode.getFirstChild();
153             if (firstChildOfCurrentNode == null) {
154                 aNode.appendChild(annotation);
155             } else {
156                 aNode.insertBefore(annotation, firstChildOfCurrentNode);
157             }
158 
159             // All Done.
160             docElement.appendChild(xsdDocumentation);
161         }
162     }
163 
164     /**
165      * Retrieves the XPath for the supplied Node within its document.
166      *
167      * @param aNode The DOM Node for which the XPath should be retrieved.
168      * @return The XPath to the supplied DOM Node.
169      */
170     public static String getXPathFor(final Node aNode) {
171 
172         List<String> nodeNameList = new ArrayList<String>();
173 
174         for (Node current = aNode; current != null; current = current.getParentNode()) {
175 
176             final String currentNodeName = current.getNodeName();
177             final String nameAttribute = DomHelper.getNameAttribute(current);
178 
179             if(currentNodeName.toLowerCase().endsWith("enumeration")) {
180 
181                 // We should print the "value" attribute here.
182                 nodeNameList.add(currentNodeName + "[@value='" + getValueAttribute(current) + "']");
183 
184             } else if(nameAttribute == null) {
185 
186                 // Just emit the node's name.
187                 nodeNameList.add(current.getNodeName());
188 
189             } else {
190 
191                 // We should print the "name" attribute here.
192                 nodeNameList.add(current.getNodeName() + "[@name='" + nameAttribute + "']");
193             }
194         }
195 
196         StringBuilder builder = new StringBuilder();
197         for (ListIterator<String> it = nodeNameList.listIterator(nodeNameList.size()); it.hasPrevious(); ) {
198             builder.append(it.previous());
199             if (it.hasPrevious()) {
200                 builder.append("/");
201             }
202         }
203 
204         return builder.toString();
205     }
206 
207     /**
208      * Retrieves the ClassLocation for the supplied aNode.
209      *
210      * @param aNode          A non-null DOM Node.
211      * @param classLocations The set of known ClassLocations, extracted from the JavaDocs.
212      * @return the ClassLocation matching the supplied Node
213      */
214     public static ClassLocation getClassLocation(final Node aNode, final Set<ClassLocation> classLocations) {
215 
216 
217         if (aNode != null) {
218 
219             // The LocalName of the supplied DOM Node should be either "complexType" or "simpleType".
220             final String nodeLocalName = aNode.getLocalName();
221             final boolean acceptableType = "complexType".equalsIgnoreCase(nodeLocalName)
222                     || "simpleType".equalsIgnoreCase(nodeLocalName);
223 
224             if (acceptableType) {
225 
226                 final String nodeClassName = DomHelper.getNameAttribute(aNode);
227                 for (ClassLocation current : classLocations) {
228 
229                     // TODO: Ensure that the namespace of the supplied aNode matches the expected namespace.
230 
231                     // Issue #25: Handle XML Type renaming.
232                     final String effectiveClassName = current.getAnnotationRenamedTo() == null
233                             ? current.getClassName()
234                             : current.getAnnotationRenamedTo();
235                     if (effectiveClassName.equalsIgnoreCase(nodeClassName)) {
236                         return current;
237                     }
238                 }
239             }
240         }
241 
242         // Nothing found
243         return null;
244     }
245 
246     /**
247      * Finds the MethodLocation within the given Set, which corresponds to the supplied DOM Node.
248      *
249      * @param aNode           A DOM Node.
250      * @param methodLocations The Set of all found/known MethodLocation instances.
251      * @return The MethodLocation matching the supplied Node - or {@code null} if no match was found.
252      */
253     public static MethodLocation getMethodLocation(final Node aNode, final Set<MethodLocation> methodLocations) {
254 
255         MethodLocation toReturn = null;
256 
257         if (aNode != null && CLASS_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
258 
259             final MethodLocation validLocation = getFieldOrMethodLocationIfValid(aNode,
260                     getContainingClassOrNull(aNode),
261                     methodLocations);
262 
263             // The MethodLocation should represent a normal getter; no arguments should be present.
264             if (validLocation != null
265                     && MethodLocation.NO_PARAMETERS.equalsIgnoreCase(validLocation.getParametersAsString())) {
266                 toReturn = validLocation;
267             }
268         }
269 
270         // All done.
271         return toReturn;
272     }
273 
274     /**
275      * Retrieves a FieldLocation from the supplied Set, provided that the FieldLocation matches the supplied Node.
276      *
277      * @param aNode          The non-null Node.
278      * @param fieldLocations The Set of known/found FieldLocation instances.
279      * @return The FieldLocation corresponding to the supplied DOM Node.
280      */
281     public static FieldLocation getFieldLocation(final Node aNode, final Set<FieldLocation> fieldLocations) {
282 
283         FieldLocation toReturn = null;
284 
285         if (aNode != null) {
286 
287             if (CLASS_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
288 
289                 // This is a ComplexType which correspond to a Java class.
290                 toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations);
291             } else if (ENUMERATION_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
292 
293                 // This is a SimpleType which correspond to a Java enum.
294                 toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations);
295             }
296         }
297 
298         // All done.
299         return toReturn;
300     }
301 
302     /**
303      * Retrieves a FieldLocation or MethodLocation from the supplied Set of Field- or MethodLocations, provided that
304      * the supplied Node has the given containing Node corresponding to a Class or an Enum.
305      *
306      * @param aNode               A non-null DOM Node.
307      * @param containingClassNode A Non-null DOM Node corresponding to a Class or Enum.
308      * @param locations           A Set containing known/found Field- and MethodLocations.
309      * @param <T>                 The FieldLocation type.
310      * @return The Matching Field- or MethodLocation.
311      */
312     public static <T extends FieldLocation> T getFieldOrMethodLocationIfValid(
313             final Node aNode,
314             final Node containingClassNode,
315             final Set<? extends FieldLocation> locations) {
316 
317         T toReturn = null;
318 
319         if (containingClassNode != null) {
320 
321             // Do we have a FieldLocation corresponding to the supplied Node?
322             for (FieldLocation current : locations) {
323 
324                 // Validate that the field and class names match the FieldLocation's corresponding values,
325                 // minding that annotations such as XmlType, XmlElement and XmlAttribute may override the
326                 // reflective Class, Field and Method names.
327                 //
328                 // Note that we cannot match package names here, as the generated XSD does not contain package
329                 // information directly. Instead, we must get the Namespace for the generated Class, and compare
330                 // it to the effective Namespace of the current Node.
331                 //
332                 // However, this is a computational-expensive operation, implying we would rather
333                 // do it at processing time when the number of nodes are (considerably?) reduced.
334 
335                 // Issue #25: Handle XML Type renaming.
336                 final String fieldName = current.getAnnotationRenamedTo() == null
337                         ? current.getMemberName()
338                         : current.getAnnotationRenamedTo();
339                 final String className = current.getClassName();
340 
341                 try {
342 
343                     //
344                     // Fields in XML enums are rendered on the form
345                     // <xs:enumeration value="LACTO_VEGETARIAN"/>, implying that
346                     // we must retrieve the 'value' attribute's value.
347                     //
348                     // Fields in XML classes are rendered on the form
349                     // <xsd:element name="Line1" type="xsd:string"/>, implying that
350                     // we must retrieve the 'name' attribute's value.
351                     //
352                     final String attributeValue = DomHelper.getNameAttribute(aNode) == null
353                             ? DomHelper.getValueAttribute(aNode)
354                             : DomHelper.getNameAttribute(aNode);
355                     if (fieldName.equalsIgnoreCase(attributeValue)
356                             && className.equalsIgnoreCase(DomHelper.getNameAttribute(containingClassNode))) {
357                         toReturn = (T) current;
358                     }
359                 } catch (Exception e) {
360                     throw new IllegalStateException("Could not acquire FieldLocation for fieldName ["
361                             + fieldName + "] and className [" + className + "]", e);
362                 }
363             }
364         }
365 
366         // All done.
367         return toReturn;
368     }
369 
370     /**
371      * Processes the supplied DOM Node, inserting XML Documentation annotations if applicable.
372      *
373      * @param aNode          The DOM Node to process.
374      * @param classJavaDocs  A Map relating {@link ClassLocation}s to {@link JavaDocData}.
375      * @param fieldJavaDocs  A Map relating {@link FieldLocation}s to {@link JavaDocData}.
376      * @param methodJavaDocs A Map relating {@link MethodLocation}s to {@link JavaDocData}.
377      * @param renderer       A non-null {@link JavaDocRenderer}.
378      */
379     public static void insertXmlDocumentationAnnotationsFor(
380             final Node aNode,
381             final SortedMap<ClassLocation, JavaDocData> classJavaDocs,
382             final SortedMap<FieldLocation, JavaDocData> fieldJavaDocs,
383             final SortedMap<MethodLocation, JavaDocData> methodJavaDocs,
384             final JavaDocRenderer renderer) {
385 
386         JavaDocData javaDocData = null;
387         SortableLocation location = null;
388 
389         // Insert the documentation annotation into the current Node.
390         final ClassLocation classLocation = DomHelper.getClassLocation(aNode, classJavaDocs.keySet());
391         if (classLocation != null) {
392             javaDocData = classJavaDocs.get(classLocation);
393             location = classLocation;
394         } else {
395 
396             final FieldLocation fieldLocation = DomHelper.getFieldLocation(aNode, fieldJavaDocs.keySet());
397             if (fieldLocation != null) {
398                 javaDocData = fieldJavaDocs.get(fieldLocation);
399                 location = fieldLocation;
400             } else {
401 
402                 final MethodLocation methodLocation = DomHelper.getMethodLocation(aNode, methodJavaDocs.keySet());
403                 if (methodLocation != null) {
404                     javaDocData = methodJavaDocs.get(methodLocation);
405                     location = methodLocation;
406                 }
407             }
408         }
409 
410         // We should have a JavaDocData here.
411         if (javaDocData == null) {
412 
413             final String nodeName = aNode.getNodeName();
414             String humanReadableName = DomHelper.getNameAttribute(aNode);
415 
416             if(humanReadableName == null && nodeName.toLowerCase().endsWith("enumeration")) {
417                 humanReadableName = "enumeration#" + getValueAttribute(aNode);
418             }
419 
420             throw new IllegalStateException("Could not find JavaDocData for XSD node ["
421                     + humanReadableName + "] with XPath [" + DomHelper.getXPathFor(aNode) + "]");
422         }
423 
424         // Add the XML documentation annotation.
425         final String processedJavaDoc = renderer.render(javaDocData, location).trim();
426         DomHelper.addXmlDocumentAnnotationTo(aNode, processedJavaDoc);
427     }
428 
429     //
430     // Private helpers
431     //
432 
433     private static Node getContainingClassOrNull(final Node aNode) {
434 
435         for (Node current = aNode.getParentNode(); current != null; current = current.getParentNode()) {
436 
437             final String localName = current.getLocalName();
438             final boolean foundClassMatch = "complexType".equalsIgnoreCase(localName)
439                     || "simpleType".equalsIgnoreCase(localName);
440 
441             if (foundClassMatch) {
442                 return current;
443             }
444         }
445 
446         // No parent Node found.
447         return null;
448     }
449 
450     private static String getNamedAttribute(final Node aNode, final String attributeName) {
451 
452         // Fail fast
453         if (aNode == null) {
454             return null;
455         }
456 
457         final NamedNodeMap attributes = aNode.getAttributes();
458         if (attributes != null) {
459 
460             final Node nameNode = attributes.getNamedItem(attributeName);
461             if (nameNode != null) {
462                 return nameNode.getNodeValue().trim();
463             }
464         }
465 
466         // Not found.
467         return null;
468     }
469 }