package org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc; import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.ClassLocation; import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.FieldLocation; import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.MethodLocation; import org.w3c.dom.CDATASection; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import javax.xml.XMLConstants; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.SortedMap; /** * Helper class stashing commonly used algorithms to work with DOM documents. * * @author <a href="mailto:[email protected]">Lennart Jörelid</a>, jGuru Europe AB * @since 2.3 */ public final class DomHelper { private static final String NAME_ATTRIBUTE = "name"; private static final String VALUE_ATTRIBUTE = "value"; /** * The name of the annotation element. */ public static final String ANNOTATION_ELEMENT_NAME = "annotation"; /** * The name of the documentation element. */ public static final String DOCUMENTATION_ELEMENT_NAME = "documentation"; /** * The namespace schema prefix for the URI {@code http://www.w3.org/2001/XMLSchema} * (i.e. {@code XMLConstants.W3C_XML_SCHEMA_NS_URI}). * * @see javax.xml.XMLConstants#W3C_XML_SCHEMA_NS_URI */ public static final String XSD_SCHEMA_NAMESPACE_PREFIX = "xs"; /** * The names of DOM Elements corresponding to Java class Fields or Methods. */ public static final List<String> CLASS_FIELD_METHOD_ELEMENT_NAMES = Arrays.asList("element", "attribute"); /** * The names of DOM Elements corresponding to Java enum Fields or Methods. */ public static final List<String> ENUMERATION_FIELD_METHOD_ELEMENT_NAMES = Collections.singletonList("enumeration"); /* * Hide constructor for utility classes */ private DomHelper() { } /** * Retrieves the value of the {@code name} attribute of the supplied Node. * * @param aNode A DOM Node. * @return the value of the {@code name} attribute of the supplied Node/Element. */ public static String getNameAttribute(final Node aNode) { return getNamedAttribute(aNode, NAME_ATTRIBUTE); } /** * Retrieves the value of the {@code value} attribute of the supplied Node. * * @param aNode A DOM Node. * @return the value of the {@code value} attribute of the supplied Node/Element. */ public static String getValueAttribute(final Node aNode) { return getNamedAttribute(aNode, VALUE_ATTRIBUTE); } /** * Checks if the supplied DOM Node is a DOM Element having a defined "name" attribute. * * @param aNode A DOM Node. * @return {@code true} if the supplied aNode is an Elemnet having a defined "name" attribute. */ public static boolean isNamedElement(final Node aNode) { final boolean isElementNode = aNode != null && aNode.getNodeType() == Node.ELEMENT_NODE; return isElementNode && getNamedAttribute(aNode, NAME_ATTRIBUTE) != null && !getNamedAttribute(aNode, NAME_ATTRIBUTE).isEmpty(); } /** * Retrieves the TagName for the supplied Node if it is an Element, and null otherwise. * * @param aNode A DOM Node. * @return The TagName of the Node if it is an Element, and null otherwise. */ public static String getElementTagName(final Node aNode) { if (aNode != null && aNode.getNodeType() == Node.ELEMENT_NODE) { final Element theElement = (Element) aNode; return theElement.getTagName(); } // The Node was not an Element. return null; } /** * <p>Adds the given formattedDocumentation within an XML documentation annotation under the supplied Node. * Only adds the documentation annotation if the formattedDocumentation is non-null and non-empty. The * documentation annotation is on the form:</p> * <pre> * <code> * <xs:annotation> * <xs:documentation>(JavaDoc here, within a CDATA section)</xs:documentation> * </xs:annotation> * </code> * </pre> * * @param aNode The non-null Node to which an XML documentation annotation should be added. * @param formattedDocumentation The documentation text to add. */ public static void addXmlDocumentAnnotationTo(final Node aNode, final String formattedDocumentation) { if (aNode != null && formattedDocumentation != null && !formattedDocumentation.isEmpty()) { // Add the new Elements, as required. final Document doc = aNode.getOwnerDocument(); final Element annotation = doc.createElementNS( XMLConstants.W3C_XML_SCHEMA_NS_URI, ANNOTATION_ELEMENT_NAME); final Element docElement = doc.createElementNS( XMLConstants.W3C_XML_SCHEMA_NS_URI, DOCUMENTATION_ELEMENT_NAME); final CDATASection xsdDocumentation = doc.createCDATASection(formattedDocumentation); // Set the prefixes annotation.setPrefix(XSD_SCHEMA_NAMESPACE_PREFIX); docElement.setPrefix(XSD_SCHEMA_NAMESPACE_PREFIX); // Inject the formattedDocumentation into the CDATA section. annotation.appendChild(docElement); final Node firstChildOfCurrentNode = aNode.getFirstChild(); if (firstChildOfCurrentNode == null) { aNode.appendChild(annotation); } else { aNode.insertBefore(annotation, firstChildOfCurrentNode); } // All Done. docElement.appendChild(xsdDocumentation); } } /** * Retrieves the XPath for the supplied Node within its document. * * @param aNode The DOM Node for which the XPath should be retrieved. * @return The XPath to the supplied DOM Node. */ public static String getXPathFor(final Node aNode) { List<String> nodeNameList = new ArrayList<String>(); for (Node current = aNode; current != null; current = current.getParentNode()) { final String currentNodeName = current.getNodeName(); final String nameAttribute = DomHelper.getNameAttribute(current); if (currentNodeName.toLowerCase().endsWith("enumeration")) { // We should print the "value" attribute here. nodeNameList.add(currentNodeName + "[@value='" + getValueAttribute(current) + "']"); } else if (nameAttribute == null) { // Just emit the node's name. nodeNameList.add(current.getNodeName()); } else { // We should print the "name" attribute here. nodeNameList.add(current.getNodeName() + "[@name='" + nameAttribute + "']"); } } StringBuilder builder = new StringBuilder(); for (ListIterator<String> it = nodeNameList.listIterator(nodeNameList.size()); it.hasPrevious(); ) { builder.append(it.previous()); if (it.hasPrevious()) { builder.append("/"); } } return builder.toString(); } /** * Retrieves the ClassLocation for the supplied aNode. * * @param aNode A non-null DOM Node. * @param classLocations The set of known ClassLocations, extracted from the JavaDocs. * @return the ClassLocation matching the supplied Node */ public static ClassLocation getClassLocation(final Node aNode, final Set<ClassLocation> classLocations) { if (aNode != null) { // The LocalName of the supplied DOM Node should be either "complexType" or "simpleType". final String nodeLocalName = aNode.getLocalName(); final boolean acceptableType = "complexType".equalsIgnoreCase(nodeLocalName) || "simpleType".equalsIgnoreCase(nodeLocalName); if (acceptableType) { final String nodeClassName = DomHelper.getNameAttribute(aNode); for (ClassLocation current : classLocations) { // TODO: Ensure that the namespace of the supplied aNode matches the expected namespace. // Issue #25: Handle XML Type renaming. final String effectiveClassName = current.getAnnotationRenamedTo() == null ? current.getClassName() : current.getAnnotationRenamedTo(); if (effectiveClassName.equalsIgnoreCase(nodeClassName)) { return current; } } } } // Nothing found return null; } /** * Finds the MethodLocation within the given Set, which corresponds to the supplied DOM Node. * * @param aNode A DOM Node. * @param methodLocations The Set of all found/known MethodLocation instances. * @return The MethodLocation matching the supplied Node - or {@code null} if no match was found. */ public static MethodLocation getMethodLocation(final Node aNode, final Set<MethodLocation> methodLocations) { MethodLocation toReturn = null; if (aNode != null && CLASS_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) { final MethodLocation validLocation = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), methodLocations); // The MethodLocation should represent a normal getter; no arguments should be present. if (validLocation != null && MethodLocation.NO_PARAMETERS.equalsIgnoreCase(validLocation.getParametersAsString())) { toReturn = validLocation; } } // All done. return toReturn; } /** * Retrieves a FieldLocation from the supplied Set, provided that the FieldLocation matches the supplied Node. * * @param aNode The non-null Node. * @param fieldLocations The Set of known/found FieldLocation instances. * @return The FieldLocation corresponding to the supplied DOM Node. */ public static FieldLocation getFieldLocation(final Node aNode, final Set<FieldLocation> fieldLocations) { FieldLocation toReturn = null; if (aNode != null) { if (CLASS_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) { // This is a ComplexType which correspond to a Java class. toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations); } else if (ENUMERATION_FIELD_METHOD_ELEMENT_NAMES.contains(aNode.getLocalName().toLowerCase())) { // This is a SimpleType which correspond to a Java enum. toReturn = getFieldOrMethodLocationIfValid(aNode, getContainingClassOrNull(aNode), fieldLocations); } } // All done. return toReturn; } /** * Retrieves a FieldLocation or MethodLocation from the supplied Set of Field- or MethodLocations, provided that * the supplied Node has the given containing Node corresponding to a Class or an Enum. * * @param aNode A non-null DOM Node. * @param containingClassNode A Non-null DOM Node corresponding to a Class or Enum. * @param locations A Set containing known/found Field- and MethodLocations. * @param <T> The FieldLocation type. * @return The Matching Field- or MethodLocation. */ public static <T extends FieldLocation> T getFieldOrMethodLocationIfValid( final Node aNode, final Node containingClassNode, final Set<? extends FieldLocation> locations) { T toReturn = null; if (containingClassNode != null) { // Do we have a FieldLocation corresponding to the supplied Node? for (FieldLocation current : locations) { // Validate that the field and class names match the FieldLocation's corresponding values, // minding that annotations such as XmlType, XmlElement and XmlAttribute may override the // reflective Class, Field and Method names. // // Note that we cannot match package names here, as the generated XSD does not contain package // information directly. Instead, we must get the Namespace for the generated Class, and compare // it to the effective Namespace of the current Node. // // However, this is a computational-expensive operation, implying we would rather // do it at processing time when the number of nodes are (considerably?) reduced. // Issue #25: Handle XML Type renaming. final String fieldName = current.getAnnotationRenamedTo() == null ? current.getMemberName() : current.getAnnotationRenamedTo(); final String className = current.getClassName(); try { // // Fields in XML enums are rendered on the form // <xs:enumeration value="LACTO_VEGETARIAN"/>, implying that // we must retrieve the 'value' attribute's value. // // Fields in XML classes are rendered on the form // <xsd:element name="Line1" type="xsd:string"/>, implying that // we must retrieve the 'name' attribute's value. // final String attributeValue = DomHelper.getNameAttribute(aNode) == null ? DomHelper.getValueAttribute(aNode) : DomHelper.getNameAttribute(aNode); if (fieldName.equalsIgnoreCase(attributeValue) && className.equalsIgnoreCase(DomHelper.getNameAttribute(containingClassNode))) { toReturn = (T) current; } } catch (Exception e) { throw new IllegalStateException("Could not acquire FieldLocation for fieldName [" + fieldName + "] and className [" + className + "]", e); } } } // All done. return toReturn; } /** * Processes the supplied DOM Node, inserting XML Documentation annotations if applicable. * * @param aNode The DOM Node to process. * @param classJavaDocs A Map relating {@link ClassLocation}s to {@link JavaDocData}. * @param fieldJavaDocs A Map relating {@link FieldLocation}s to {@link JavaDocData}. * @param methodJavaDocs A Map relating {@link MethodLocation}s to {@link JavaDocData}. * @param renderer A non-null {@link JavaDocRenderer}. */ public static void insertXmlDocumentationAnnotationsFor( final Node aNode, final SortedMap<ClassLocation, JavaDocData> classJavaDocs, final SortedMap<FieldLocation, JavaDocData> fieldJavaDocs, final SortedMap<MethodLocation, JavaDocData> methodJavaDocs, final JavaDocRenderer renderer) { JavaDocData javaDocData = null; SortableLocation location = null; // Insert the documentation annotation into the current Node. final ClassLocation classLocation = DomHelper.getClassLocation(aNode, classJavaDocs.keySet()); if (classLocation != null) { javaDocData = classJavaDocs.get(classLocation); location = classLocation; } else { final FieldLocation fieldLocation = DomHelper.getFieldLocation(aNode, fieldJavaDocs.keySet()); if (fieldLocation != null) { javaDocData = fieldJavaDocs.get(fieldLocation); location = fieldLocation; } else { final MethodLocation methodLocation = DomHelper.getMethodLocation(aNode, methodJavaDocs.keySet()); if (methodLocation != null) { javaDocData = methodJavaDocs.get(methodLocation); location = methodLocation; } } } // We should have a JavaDocData here. if (javaDocData == null) { final String nodeName = aNode.getNodeName(); String humanReadableName = DomHelper.getNameAttribute(aNode); if (humanReadableName == null && nodeName.toLowerCase().endsWith("enumeration")) { humanReadableName = "enumeration#" + getValueAttribute(aNode); } throw new IllegalStateException("Could not find JavaDocData for XSD node [" + humanReadableName + "] with XPath [" + DomHelper.getXPathFor(aNode) + "]"); } // Add the XML documentation annotation. final String processedJavaDoc = renderer.render(javaDocData, location).trim(); DomHelper.addXmlDocumentAnnotationTo(aNode, processedJavaDoc); } // // Private helpers // private static Node getContainingClassOrNull(final Node aNode) { for (Node current = aNode.getParentNode(); current != null; current = current.getParentNode()) { final String localName = current.getLocalName(); final boolean foundClassMatch = "complexType".equalsIgnoreCase(localName) || "simpleType".equalsIgnoreCase(localName); if (foundClassMatch) { return current; } } // No parent Node found. return null; } private static String getNamedAttribute(final Node aNode, final String attributeName) { // Fail fast if (aNode == null) { return null; } final NamedNodeMap attributes = aNode.getAttributes(); if (attributes != null) { final Node nameNode = attributes.getNamedItem(attributeName); if (nameNode != null) { return nameNode.getNodeValue().trim(); } } // Not found. return null; } }