package org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc;
import org.codehaus.mojo.jaxb2.AbstractJaxbMojo;
import org.codehaus.mojo.jaxb2.BufferingLog;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.NodeProcessor;
import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
import org.codehaus.mojo.jaxb2.shared.Validate;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.Assert;
import org.junit.Before;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
public abstract class AbstractSourceCodeAwareNodeProcessingTest {
/**
* Default systemId for the empty namespace.
*/
public static final String DEFAULT_EMPTY_NAMESPACE_SYSTEM_ID = "emptyNamespaceSystemId.xsd";
// Shared state
protected BufferingLog log;
protected SearchableDocumentation docs;
protected SortedMap<String, String> namespace2GeneratedSchemaMap;
protected SortedMap<String, Document> namespace2DocumentMap;
protected Map<String, String> namespace2SystemIdMap;
protected List<String> xsdGenerationWarnings;
protected final File basedir;
protected final File testJavaDir;
protected JAXBContext jaxbContext;
protected SortedMap<String, Throwable> xsdGenerationLog;
// Internal state
private List<Class<?>> jaxbClasses;
public AbstractSourceCodeAwareNodeProcessingTest() {
// Setup the basic directories.
basedir = getBasedir();
testJavaDir = new File(basedir, "src/test/java");
Assert.assertTrue(testJavaDir.exists() && testJavaDir.isDirectory());
}
@Before
public final void setupSharedState() throws Exception {
log = new BufferingLog(BufferingLog.LogLevel.DEBUG);
// Create internal state for the generated structures.
namespace2SystemIdMap = new TreeMap<String, String>();
xsdGenerationWarnings = new ArrayList<String>();
namespace2DocumentMap = new TreeMap<String, Document>();
namespace2GeneratedSchemaMap = new TreeMap<String, String>();
// Pre-populate the namespace2SystemIdMap
namespace2SystemIdMap.put(SomewhatNamedPerson.NAMESPACE, "somewhatNamedPerson.xsd");
namespace2SystemIdMap.put("http://jaxb.mojohaus.org/wrappers", "wrapperExample.xsd");
namespace2SystemIdMap.put("", DEFAULT_EMPTY_NAMESPACE_SYSTEM_ID);
// Create the JAXBContext
jaxbClasses = getJaxbAnnotatedClassesForJaxbContext();
Assert.assertNotNull("getJaxbAnnotatedClassesForJaxbContext() should not return a null List.", jaxbClasses);
final Class<?>[] classArray = jaxbClasses.toArray(new Class<?>[jaxbClasses.size()]);
jaxbContext = JAXBContext.newInstance(classArray);
// Generate the vanilla XSD from JAXB
final SortedMap<String, StringWriter> tmpSchemaMap = new TreeMap<String, StringWriter>();
try {
jaxbContext.generateSchema(new SchemaOutputResolver() {
@Override
public Result createOutput(final String namespaceUri,
final String suggestedFileName)
throws IOException {
// As put in the XmlBinding JAXB implementation of Nazgul Core:
//
// "The types should really be annotated with @XmlType(namespace = "... something ...")
// to avoid using the default ("") namespace".
if (namespaceUri.isEmpty()) {
xsdGenerationWarnings.add("Got empty namespaceUri for suggestedFileName ["
+ suggestedFileName + "].");
}
// Create the result Writer
final StringWriter out = new StringWriter();
final StreamResult toReturn = new StreamResult(out);
// The systemId *must* be non-null, even in this case where we
// do not write the XSD to a file.
final String effectiveSystemId = namespace2SystemIdMap.get(namespaceUri) == null
? suggestedFileName
: namespace2SystemIdMap.get(namespaceUri);
toReturn.setSystemId(effectiveSystemId);
// Map the namespaceUri to the schemaResult.
tmpSchemaMap.put(namespaceUri, out);
// All done.
return toReturn;
}
});
} catch (IOException e) {
throw new IllegalArgumentException("Could not acquire Schema snippets.", e);
}
// Store all generated XSDs
for (Map.Entry<String, StringWriter> current : tmpSchemaMap.entrySet()) {
namespace2GeneratedSchemaMap.put(current.getKey(), current.getValue().toString());
}
// Create XML Documents for all generated Schemas
for (Map.Entry<String, String> current : namespace2GeneratedSchemaMap.entrySet()) {
final Document document = createDocument(current.getValue());
namespace2DocumentMap.put(current.getKey(), document);
}
// Create the SearchableDocumentation
final JavaDocExtractor extractor = new JavaDocExtractor(log);
extractor.addSourceFiles(resolveSourceFiles());
docs = extractor.process();
// Stash and clear the log buffer.
xsdGenerationLog = log.getAndResetLogBuffer();
}
/**
* @return A List containing all classes which should be part of the JAXBContext.
*/
protected abstract List<Class<?>> getJaxbAnnotatedClassesForJaxbContext();
/**
* @return The basedir directory, corresponding to the root of this project.
*/
protected File getBasedir() {
// Use the system property if available.
String basedirPath = System.getProperty("basedir");
if (basedirPath == null) {
basedirPath = new File("").getAbsolutePath();
}
final File toReturn = new File(basedirPath);
Assert.assertNotNull("Could not find 'basedir'. Please set the system property 'basedir'.", toReturn);
Assert.assertTrue("'basedir' must be an existing directory. ", toReturn.exists() && toReturn.isDirectory());
// All done.
return toReturn;
}
/**
* Creates a DOM Document from the supplied XML.
*
* @param xmlContent The non-empty XML which should be converted into a Document.
* @return The Document created from the supplied XML Content.
*/
protected final Document createDocument(final String xmlContent) {
// Check sanity
Validate.notEmpty(xmlContent, "xmlContent");
// Build a DOM model of the provided xmlFileStream.
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
try {
return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xmlContent)));
} catch (Exception e) {
throw new IllegalArgumentException("Could not create DOM Document", e);
}
}
/**
* Drives the supplied visitor to process the provided Node and all its children, should the recurseToChildren flag
* be set to <code>true</code>. All attributes of the current node are processed before recursing to children (i.e.
* breadth first recursion).
*
* @param node The Node to process.
* @param recurseToChildren if <code>true</code>, processes all children of the supplied node recursively.
* @param visitor The NodeProcessor instance which should process the nodes.
*/
public final void process(final Node node, final boolean recurseToChildren, final NodeProcessor visitor) {
// Process the current Node, if the NodeProcessor accepts it.
if (visitor.accept(node)) {
onAcceptedNode(node);
visitor.process(node);
}
NamedNodeMap attributes = node.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attribute = attributes.item(i);
// Process the current attribute, if the NodeProcessor accepts it.
if (visitor.accept(attribute)) {
onAcceptedAttribute(attribute);
visitor.process(attribute);
}
}
if (recurseToChildren) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
// Recurse to Element children.
if (child.getNodeType() == Node.ELEMENT_NODE) {
process(child, true, visitor);
}
}
}
}
/**
* Event callback when a nodeProcessor has accepted a Node.
*
* @param aNode the accepted Node
*/
protected void onAcceptedNode(final Node aNode) {
// name="firstName"
final String nodeName = aNode.getAttributes().getNamedItem("name").getNodeValue();
log.info("Accepted node [" + aNode.getNodeName() + "] " + nodeName);
}
/**
* Event callback when a nodeProcessor has accepted an Attribute.
*
* @param anAttribute the accepted attribute.
*/
protected void onAcceptedAttribute(final Node anAttribute) {
log.info("Accepted attribute [" + anAttribute.getNodeName() + "]");
}
//
// Private helpers
//
/**
* Utility method to read all (string formatted) data from the given classpath-relative
* file and return the data as a string.
*
* @param path The classpath-relative file path.
* @return The content of the supplied file.
*/
protected static String readFully(final String path) {
final StringBuilder toReturn = new StringBuilder(50);
try {
// Will produce a NPE if the path was not directed to a file.
final InputStream resource = AbstractSourceCodeAwareNodeProcessingTest
.class
.getClassLoader()
.getResourceAsStream(path);
final BufferedReader tmp = new BufferedReader(new InputStreamReader(resource));
for (String line = tmp.readLine(); line != null; line = tmp.readLine()) {
toReturn.append(line).append(AbstractJaxbMojo.NEWLINE);
}
} catch (final Exception e) {
throw new IllegalArgumentException("Resource [" + path + "] not readable.");
}
// All done.
return toReturn.toString();
}
/**
* Compares XML documents provided by the two Readers.
*
* @param expected The expected document data.
* @param actual The actual document data.
* @return A DetailedDiff object, describing all differences in documents supplied.
* @throws org.xml.sax.SAXException If a SAXException was raised during parsing of the two Documents.
* @throws IOException If an I/O-related exception was raised while acquiring the data from the Readers.
*/
protected static Diff compareXmlIgnoringWhitespace(final String expected, final String actual) throws SAXException,
IOException {
// Check sanity
org.apache.commons.lang3.Validate.notNull(expected, "Cannot handle null expected argument.");
org.apache.commons.lang3.Validate.notNull(actual, "Cannot handle null actual argument.");
// Ignore whitespace - and also normalize the Documents.
XMLUnit.setNormalize(true);
XMLUnit.setIgnoreWhitespace(true);
XMLUnit.setNormalize(true);
// Compare and return
return XMLUnit.compareXML(expected, actual);
}
private List<File> resolveSourceFiles() {
final List<File> sourceDirs = Arrays.<File>asList(new File(basedir, "src/main/java"), testJavaDir);
final List<File> candidates = FileSystemUtilities.resolveRecursively(sourceDirs, null, log);
final List<File> toReturn = new ArrayList<File>();
for (File current : candidates) {
for (Class<?> currentClass : jaxbClasses) {
final String expectedFileName = currentClass.getSimpleName() + ".java";
if (expectedFileName.equalsIgnoreCase(current.getName())) {
final String transmutedCanonicalPath = FileSystemUtilities.getCanonicalPath(current)
.replace("/", ".")
.replace(File.separator, ".");
if (transmutedCanonicalPath.contains(currentClass.getPackage().getName())) {
toReturn.add(current);
}
}
}
}
// All done.
return toReturn;
}
/**
* Prints the content of the supplied DOM Document as a string.
*
* @param doc A non-null DOM Document.
* @return A String holding the pretty-printed version of the supplied doc.
*/
public static String printDocument(final Document doc) {
try {
// Create the Unity-Transformer
final TransformerFactory tf = TransformerFactory.newInstance();
final Transformer transformer = tf.newTransformer();
// Make it pretty print stuff.
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
// Harvest the result, and return.
final StringWriter out = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(out));
return out.toString();
} catch (Exception e) {
throw new IllegalArgumentException("Could not print document", e);
}
}
}