Coverage Report - org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.DomHelper
 
Classes in this File Line Coverage Branch Coverage Complexity
DomHelper
87 %
113/129
78 %
77/98
4,786
 
 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  1
     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  1
     public static final List<String> ENUMERATION_FIELD_METHOD_ELEMENT_NAMES = Collections.singletonList("enumeration");
 59  
 
 60  
     /*
 61  
      * Hide constructor for utility classes
 62  
      */
 63  0
     private DomHelper() {
 64  0
     }
 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  718
         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  174
         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  176
         final boolean isElementNode = aNode != null && aNode.getNodeType() == Node.ELEMENT_NODE;
 95  
 
 96  352
         return isElementNode
 97  89
                 && getNamedAttribute(aNode, NAME_ATTRIBUTE) != null
 98  27
                 && !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  0
         if (aNode != null && aNode.getNodeType() == Node.ELEMENT_NODE) {
 110  
 
 111  0
             final Element theElement = (Element) aNode;
 112  0
             return theElement.getTagName();
 113  
         }
 114  
 
 115  
         // The Node was not an Element.
 116  0
         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  35
         if (aNode != null && formattedDocumentation != null && !formattedDocumentation.isEmpty()) {
 137  
 
 138  
             // Add the new Elements, as required.
 139  33
             final Document doc = aNode.getOwnerDocument();
 140  33
             final Element annotation = doc.createElementNS(
 141  
                     XMLConstants.W3C_XML_SCHEMA_NS_URI, ANNOTATION_ELEMENT_NAME);
 142  33
             final Element docElement = doc.createElementNS(
 143  
                     XMLConstants.W3C_XML_SCHEMA_NS_URI, DOCUMENTATION_ELEMENT_NAME);
 144  33
             final CDATASection xsdDocumentation = doc.createCDATASection(formattedDocumentation);
 145  
 
 146  
             // Set the prefixes
 147  33
             annotation.setPrefix(XSD_SCHEMA_NAMESPACE_PREFIX);
 148  33
             docElement.setPrefix(XSD_SCHEMA_NAMESPACE_PREFIX);
 149  
 
 150  
             // Inject the formattedDocumentation into the CDATA section.
 151  33
             annotation.appendChild(docElement);
 152  33
             final Node firstChildOfCurrentNode = aNode.getFirstChild();
 153  33
             if (firstChildOfCurrentNode == null) {
 154  17
                 aNode.appendChild(annotation);
 155  
             } else {
 156  16
                 aNode.insertBefore(annotation, firstChildOfCurrentNode);
 157  
             }
 158  
 
 159  
             // All Done.
 160  33
             docElement.appendChild(xsdDocumentation);
 161  
         }
 162  35
     }
 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  3
         List<String> nodeNameList = new ArrayList<String>();
 173  
 
 174  18
         for (Node current = aNode; current != null; current = current.getParentNode()) {
 175  
 
 176  15
             final String currentNodeName = current.getNodeName();
 177  15
             final String nameAttribute = DomHelper.getNameAttribute(current);
 178  
 
 179  15
             if (currentNodeName.toLowerCase().endsWith("enumeration")) {
 180  
 
 181  
                 // We should print the "value" attribute here.
 182  3
                 nodeNameList.add(currentNodeName + "[@value='" + getValueAttribute(current) + "']");
 183  
 
 184  12
             } else if (nameAttribute == null) {
 185  
 
 186  
                 // Just emit the node's name.
 187  9
                 nodeNameList.add(current.getNodeName());
 188  
 
 189  
             } else {
 190  
 
 191  
                 // We should print the "name" attribute here.
 192  3
                 nodeNameList.add(current.getNodeName() + "[@name='" + nameAttribute + "']");
 193  
             }
 194  
         }
 195  
 
 196  3
         StringBuilder builder = new StringBuilder();
 197  3
         for (ListIterator<String> it = nodeNameList.listIterator(nodeNameList.size()); it.hasPrevious(); ) {
 198  15
             builder.append(it.previous());
 199  15
             if (it.hasPrevious()) {
 200  12
                 builder.append("/");
 201  
             }
 202  
         }
 203  
 
 204  3
         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  52
         if (aNode != null) {
 218  
 
 219  
             // The LocalName of the supplied DOM Node should be either "complexType" or "simpleType".
 220  52
             final String nodeLocalName = aNode.getLocalName();
 221  52
             final boolean acceptableType = "complexType".equalsIgnoreCase(nodeLocalName)
 222  42
                     || "simpleType".equalsIgnoreCase(nodeLocalName);
 223  
 
 224  52
             if (acceptableType) {
 225  
 
 226  18
                 final String nodeClassName = DomHelper.getNameAttribute(aNode);
 227  18
                 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  30
                     final String effectiveClassName = current.getAnnotationRenamedTo() == null
 233  30
                             ? current.getClassName()
 234  0
                             : current.getAnnotationRenamedTo();
 235  30
                     if (effectiveClassName.equalsIgnoreCase(nodeClassName)) {
 236  18
                         return current;
 237  
                     }
 238  12
                 }
 239  
             }
 240  
         }
 241  
 
 242  
         // Nothing found
 243  34
         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  28
         MethodLocation toReturn = null;
 256  
 
 257  28
         if (aNode != null && CLASS_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
 258  
 
 259  42
             final MethodLocation validLocation = getFieldOrMethodLocationIfValid(aNode,
 260  21
                     getContainingClassOrNull(aNode),
 261  
                     methodLocations);
 262  
 
 263  
             // The MethodLocation should represent a normal getter; no arguments should be present.
 264  21
             if (validLocation != null
 265  2
                     && MethodLocation.NO_PARAMETERS.equalsIgnoreCase(validLocation.getParametersAsString())) {
 266  2
                 toReturn = validLocation;
 267  
             }
 268  
         }
 269  
 
 270  
         // All done.
 271  28
         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  50
         FieldLocation toReturn = null;
 284  
 
 285  50
         if (aNode != null) {
 286  
 
 287  50
             if (CLASS_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
 288  
 
 289  
                 // This is a ComplexType which correspond to a Java class.
 290  29
                 toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations);
 291  21
             } else if (ENUMERATION_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) {
 292  
 
 293  
                 // This is a SimpleType which correspond to a Java enum.
 294  14
                 toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations);
 295  
             }
 296  
         }
 297  
 
 298  
         // All done.
 299  50
         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  64
         T toReturn = null;
 318  
 
 319  64
         if (containingClassNode != null) {
 320  
 
 321  
             // Do we have a FieldLocation corresponding to the supplied Node?
 322  58
             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  406
                 final String fieldName = current.getAnnotationRenamedTo() == null
 337  236
                         ? current.getMemberName()
 338  170
                         : current.getAnnotationRenamedTo();
 339  406
                 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  406
                     final String attributeValue = DomHelper.getNameAttribute(aNode) == null
 353  168
                             ? DomHelper.getValueAttribute(aNode)
 354  238
                             : DomHelper.getNameAttribute(aNode);
 355  406
                     if (fieldName.equalsIgnoreCase(attributeValue)
 356  37
                             && className.equalsIgnoreCase(DomHelper.getNameAttribute(containingClassNode))) {
 357  34
                         toReturn = (T) current;
 358  
                     }
 359  0
                 } catch (Exception e) {
 360  0
                     throw new IllegalStateException("Could not acquire FieldLocation for fieldName ["
 361  
                             + fieldName + "] and className [" + className + "]", e);
 362  406
                 }
 363  406
             }
 364  
         }
 365  
 
 366  
         // All done.
 367  64
         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  35
         JavaDocData javaDocData = null;
 387  35
         SortableLocation location = null;
 388  
 
 389  
         // Insert the documentation annotation into the current Node.
 390  35
         final ClassLocation classLocation = DomHelper.getClassLocation(aNode, classJavaDocs.keySet());
 391  35
         if (classLocation != null) {
 392  11
             javaDocData = classJavaDocs.get(classLocation);
 393  11
             location = classLocation;
 394  
         } else {
 395  
 
 396  24
             final FieldLocation fieldLocation = DomHelper.getFieldLocation(aNode, fieldJavaDocs.keySet());
 397  24
             if (fieldLocation != null) {
 398  23
                 javaDocData = fieldJavaDocs.get(fieldLocation);
 399  23
                 location = fieldLocation;
 400  
             } else {
 401  
 
 402  1
                 final MethodLocation methodLocation = DomHelper.getMethodLocation(aNode, methodJavaDocs.keySet());
 403  1
                 if (methodLocation != null) {
 404  1
                     javaDocData = methodJavaDocs.get(methodLocation);
 405  1
                     location = methodLocation;
 406  
                 }
 407  
             }
 408  
         }
 409  
 
 410  
         // We should have a JavaDocData here.
 411  35
         if (javaDocData == null) {
 412  
 
 413  0
             final String nodeName = aNode.getNodeName();
 414  0
             String humanReadableName = DomHelper.getNameAttribute(aNode);
 415  
 
 416  0
             if (humanReadableName == null && nodeName.toLowerCase().endsWith("enumeration")) {
 417  0
                 humanReadableName = "enumeration#" + getValueAttribute(aNode);
 418  
             }
 419  
 
 420  0
             throw new IllegalStateException("Could not find JavaDocData for XSD node ["
 421  0
                     + humanReadableName + "] with XPath [" + DomHelper.getXPathFor(aNode) + "]");
 422  
         }
 423  
 
 424  
         // Add the XML documentation annotation.
 425  35
         final String processedJavaDoc = renderer.render(javaDocData, location).trim();
 426  35
         DomHelper.addXmlDocumentAnnotationTo(aNode, processedJavaDoc);
 427  35
     }
 428  
 
 429  
     //
 430  
     // Private helpers
 431  
     //
 432  
 
 433  
     private static Node getContainingClassOrNull(final Node aNode) {
 434  
 
 435  131
         for (Node current = aNode.getParentNode(); current != null; current = current.getParentNode()) {
 436  
 
 437  125
             final String localName = current.getLocalName();
 438  125
             final boolean foundClassMatch = "complexType".equalsIgnoreCase(localName)
 439  81
                     || "simpleType".equalsIgnoreCase(localName);
 440  
 
 441  125
             if (foundClassMatch) {
 442  58
                 return current;
 443  
             }
 444  
         }
 445  
 
 446  
         // No parent Node found.
 447  6
         return null;
 448  
     }
 449  
 
 450  
     private static String getNamedAttribute(final Node aNode, final String attributeName) {
 451  
 
 452  
         // Fail fast
 453  1008
         if (aNode == null) {
 454  0
             return null;
 455  
         }
 456  
 
 457  1008
         final NamedNodeMap attributes = aNode.getAttributes();
 458  1008
         if (attributes != null) {
 459  
 
 460  1005
             final Node nameNode = attributes.getNamedItem(attributeName);
 461  1005
             if (nameNode != null) {
 462  765
                 return nameNode.getNodeValue().trim();
 463  
             }
 464  
         }
 465  
 
 466  
         // Not found.
 467  243
         return null;
 468  
     }
 469  
 }