View Javadoc
1   package org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc;
2   
3   import org.codehaus.mojo.jaxb2.AbstractJaxbMojo;
4   import org.codehaus.mojo.jaxb2.BufferingLog;
5   import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.NodeProcessor;
6   import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
7   import org.codehaus.mojo.jaxb2.shared.Validate;
8   import org.custommonkey.xmlunit.Diff;
9   import org.custommonkey.xmlunit.XMLUnit;
10  import org.junit.Assert;
11  import org.junit.Before;
12  import org.w3c.dom.Document;
13  import org.w3c.dom.NamedNodeMap;
14  import org.w3c.dom.Node;
15  import org.w3c.dom.NodeList;
16  import org.xml.sax.InputSource;
17  import org.xml.sax.SAXException;
18  
19  import javax.xml.bind.JAXBContext;
20  import javax.xml.bind.SchemaOutputResolver;
21  import javax.xml.parsers.DocumentBuilderFactory;
22  import javax.xml.transform.OutputKeys;
23  import javax.xml.transform.Result;
24  import javax.xml.transform.Transformer;
25  import javax.xml.transform.TransformerFactory;
26  import javax.xml.transform.dom.DOMSource;
27  import javax.xml.transform.stream.StreamResult;
28  import java.io.BufferedReader;
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.InputStreamReader;
33  import java.io.StringReader;
34  import java.io.StringWriter;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.SortedMap;
40  import java.util.TreeMap;
41  
42  /**
43   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>, jGuru Europe AB
44   */
45  public abstract class AbstractSourceCodeAwareNodeProcessingTest {
46  
47      /**
48       * Default systemId for the empty namespace.
49       */
50      public static final String DEFAULT_EMPTY_NAMESPACE_SYSTEM_ID = "emptyNamespaceSystemId.xsd";
51  
52      // Shared state
53      protected BufferingLog log;
54      protected SearchableDocumentation docs;
55      protected SortedMap<String, String> namespace2GeneratedSchemaMap;
56      protected SortedMap<String, Document> namespace2DocumentMap;
57      protected Map<String, String> namespace2SystemIdMap;
58      protected List<String> xsdGenerationWarnings;
59      protected final File basedir;
60      protected final File testJavaDir;
61      protected JAXBContext jaxbContext;
62      protected SortedMap<String, Throwable> xsdGenerationLog;
63  
64      // Internal state
65      private List<Class<?>> jaxbClasses;
66  
67      public AbstractSourceCodeAwareNodeProcessingTest() {
68  
69          // Setup the basic directories.
70          basedir = getBasedir();
71          testJavaDir = new File(basedir, "src/test/java");
72          Assert.assertTrue(testJavaDir.exists() && testJavaDir.isDirectory());
73      }
74  
75      @Before
76      public final void setupSharedState() throws Exception {
77  
78          log = new BufferingLog(BufferingLog.LogLevel.DEBUG);
79  
80          // Create internal state for the generated structures.
81          namespace2SystemIdMap = new TreeMap<String, String>();
82          xsdGenerationWarnings = new ArrayList<String>();
83          namespace2DocumentMap = new TreeMap<String, Document>();
84          namespace2GeneratedSchemaMap = new TreeMap<String, String>();
85  
86          // Pre-populate the namespace2SystemIdMap
87          namespace2SystemIdMap.put(SomewhatNamedPerson.NAMESPACE, "somewhatNamedPerson.xsd");
88          namespace2SystemIdMap.put("http://jaxb.mojohaus.org/wrappers", "wrapperExample.xsd");
89          namespace2SystemIdMap.put("http://gnat.west.se/foods", "anotherExample.xsd");
90          namespace2SystemIdMap.put("", DEFAULT_EMPTY_NAMESPACE_SYSTEM_ID);
91  
92          // Create the JAXBContext
93          jaxbClasses = getJaxbAnnotatedClassesForJaxbContext();
94          Assert.assertNotNull("getJaxbAnnotatedClassesForJaxbContext() should not return a null List.", jaxbClasses);
95          final Class<?>[] classArray = jaxbClasses.toArray(new Class<?>[jaxbClasses.size()]);
96          jaxbContext = JAXBContext.newInstance(classArray);
97  
98          // Generate the vanilla XSD from JAXB
99          final SortedMap<String, StringWriter> tmpSchemaMap = new TreeMap<String, StringWriter>();
100 
101         try {
102             jaxbContext.generateSchema(new SchemaOutputResolver() {
103                 @Override
104                 public Result createOutput(final String namespaceUri,
105                                            final String suggestedFileName)
106                         throws IOException {
107 
108                     // As put in the XmlBinding JAXB implementation of Nazgul Core:
109                     //
110                     // "The types should really be annotated with @XmlType(namespace = "... something ...")
111                     // to avoid using the default ("") namespace".
112                     if (namespaceUri.isEmpty()) {
113                         xsdGenerationWarnings.add("Got empty namespaceUri for suggestedFileName ["
114                                 + suggestedFileName + "].");
115                     }
116 
117                     // Create the result Writer
118                     final StringWriter out = new StringWriter();
119                     final StreamResult toReturn = new StreamResult(out);
120 
121                     // The systemId *must* be non-null, even in this case where we
122                     // do not write the XSD to a file.
123                     final String effectiveSystemId = namespace2SystemIdMap.get(namespaceUri) == null
124                             ? suggestedFileName
125                             : namespace2SystemIdMap.get(namespaceUri);
126                     toReturn.setSystemId(effectiveSystemId);
127 
128                     // Map the namespaceUri to the schemaResult.
129                     tmpSchemaMap.put(namespaceUri, out);
130 
131                     // All done.
132                     return toReturn;
133                 }
134             });
135         } catch (IOException e) {
136             throw new IllegalArgumentException("Could not acquire Schema snippets.", e);
137         }
138 
139         // Store all generated XSDs
140         for (Map.Entry<String, StringWriter> current : tmpSchemaMap.entrySet()) {
141             namespace2GeneratedSchemaMap.put(current.getKey(), current.getValue().toString());
142         }
143 
144         // Create XML Documents for all generated Schemas
145         for (Map.Entry<String, String> current : namespace2GeneratedSchemaMap.entrySet()) {
146             final Document document = createDocument(current.getValue());
147             namespace2DocumentMap.put(current.getKey(), document);
148         }
149 
150         // Create the SearchableDocumentation
151         final JavaDocExtractor extractor = new JavaDocExtractor(log);
152         extractor.addSourceFiles(resolveSourceFiles());
153         docs = extractor.process();
154 
155         // Stash and clear the log buffer.
156         xsdGenerationLog = log.getAndResetLogBuffer();
157     }
158 
159     /**
160      * @return A List containing all classes which should be part of the JAXBContext.
161      */
162     protected abstract List<Class<?>> getJaxbAnnotatedClassesForJaxbContext();
163 
164     /**
165      * @return The basedir directory, corresponding to the root of this project.
166      */
167     protected File getBasedir() {
168 
169         // Use the system property if available.
170         String basedirPath = System.getProperty("basedir");
171         if (basedirPath == null) {
172             basedirPath = new File("").getAbsolutePath();
173         }
174 
175         final File toReturn = new File(basedirPath);
176         Assert.assertNotNull("Could not find 'basedir'. Please set the system property 'basedir'.", toReturn);
177         Assert.assertTrue("'basedir' must be an existing directory. ", toReturn.exists() && toReturn.isDirectory());
178 
179         // All done.
180         return toReturn;
181     }
182 
183     /**
184      * Creates a DOM Document from the supplied XML.
185      *
186      * @param xmlContent The non-empty XML which should be converted into a Document.
187      * @return The Document created from the supplied XML Content.
188      */
189     protected final Document createDocument(final String xmlContent) {
190 
191         // Check sanity
192         Validate.notEmpty(xmlContent, "xmlContent");
193 
194         // Build a DOM model of the provided xmlFileStream.
195         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
196         factory.setNamespaceAware(true);
197 
198         try {
199             return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xmlContent)));
200         } catch (Exception e) {
201             throw new IllegalArgumentException("Could not create DOM Document", e);
202         }
203     }
204 
205     /**
206      * Drives the supplied visitor to process the provided Node and all its children, should the recurseToChildren flag
207      * be set to <code>true</code>. All attributes of the current node are processed before recursing to children (i.e.
208      * breadth first recursion).
209      *
210      * @param node              The Node to process.
211      * @param recurseToChildren if <code>true</code>, processes all children of the supplied node recursively.
212      * @param visitor           The NodeProcessor instance which should process the nodes.
213      */
214     public final void process(final Node node, final boolean recurseToChildren, final NodeProcessor visitor) {
215 
216         // Process the current Node, if the NodeProcessor accepts it.
217         if (visitor.accept(node)) {
218             onAcceptedNode(node);
219             visitor.process(node);
220         }
221 
222         NamedNodeMap attributes = node.getAttributes();
223         for (int i = 0; i < attributes.getLength(); i++) {
224             Node attribute = attributes.item(i);
225 
226             // Process the current attribute, if the NodeProcessor accepts it.
227             if (visitor.accept(attribute)) {
228                 onAcceptedAttribute(attribute);
229                 visitor.process(attribute);
230             }
231         }
232 
233         if (recurseToChildren) {
234             NodeList children = node.getChildNodes();
235             for (int i = 0; i < children.getLength(); i++) {
236                 Node child = children.item(i);
237 
238                 // Recurse to Element children.
239                 if (child.getNodeType() == Node.ELEMENT_NODE) {
240                     process(child, true, visitor);
241                 }
242             }
243         }
244     }
245 
246     /**
247      * Event callback when a nodeProcessor has accepted a Node.
248      *
249      * @param aNode the accepted Node
250      */
251     protected void onAcceptedNode(final Node aNode) {
252         // name="firstName"
253 
254         final Node nameAttribute = aNode.getAttributes().getNamedItem("name");
255         if(nameAttribute != null) {
256 
257             final String nodeName = nameAttribute.getNodeValue();
258             log.info("Accepted node [" + aNode.getNodeName() + "] " + nodeName);
259         }
260     }
261 
262     /**
263      * Event callback when a nodeProcessor has accepted an Attribute.
264      *
265      * @param anAttribute the accepted attribute.
266      */
267     protected void onAcceptedAttribute(final Node anAttribute) {
268         log.info("Accepted attribute [" + anAttribute.getNodeName() + "]");
269     }
270 
271     //
272     // Private helpers
273     //
274 
275     /**
276      * Utility method to read all (string formatted) data from the given classpath-relative
277      * file and return the data as a string.
278      *
279      * @param path The classpath-relative file path.
280      * @return The content of the supplied file.
281      */
282     protected static String readFully(final String path) {
283 
284         final StringBuilder toReturn = new StringBuilder(50);
285 
286         try {
287 
288             // Will produce a NPE if the path was not directed to a file.
289             final InputStream resource = AbstractSourceCodeAwareNodeProcessingTest
290                     .class
291                     .getClassLoader()
292                     .getResourceAsStream(path);
293             final BufferedReader tmp = new BufferedReader(new InputStreamReader(resource));
294 
295             for (String line = tmp.readLine(); line != null; line = tmp.readLine()) {
296                 toReturn.append(line).append(AbstractJaxbMojo.NEWLINE);
297             }
298         } catch (final Exception e) {
299             throw new IllegalArgumentException("Resource [" + path + "] not readable.");
300         }
301 
302         // All done.
303         return toReturn.toString();
304     }
305 
306     /**
307      * Compares XML documents provided by the two Readers.
308      *
309      * @param expected The expected document data.
310      * @param actual   The actual document data.
311      * @return A DetailedDiff object, describing all differences in documents supplied.
312      * @throws org.xml.sax.SAXException If a SAXException was raised during parsing of the two Documents.
313      * @throws IOException              If an I/O-related exception was raised while acquiring the data from the Readers.
314      */
315     protected static Diff compareXmlIgnoringWhitespace(final String expected, final String actual) throws SAXException,
316             IOException {
317 
318         // Check sanity
319         Validate.notNull(expected, "Cannot handle null expected argument.");
320         Validate.notNull(actual, "Cannot handle null actual argument.");
321 
322         // Ignore whitespace - and also normalize the Documents.
323         XMLUnit.setNormalize(true);
324         XMLUnit.setIgnoreWhitespace(true);
325         XMLUnit.setNormalize(true);
326 
327         // Compare and return
328         return XMLUnit.compareXML(expected, actual);
329     }
330 
331     private List<File> resolveSourceFiles() {
332 
333         final List<File> sourceDirs = Arrays.<File>asList(new File(basedir, "src/main/java"), testJavaDir);
334         final List<File> candidates = FileSystemUtilities.resolveRecursively(sourceDirs, null, log);
335         final List<File> toReturn = new ArrayList<File>();
336 
337         for (File current : candidates) {
338             for (Class<?> currentClass : jaxbClasses) {
339 
340                 final String expectedFileName = currentClass.getSimpleName() + ".java";
341                 if (expectedFileName.equalsIgnoreCase(current.getName())) {
342 
343                     final String transmutedCanonicalPath = FileSystemUtilities.getCanonicalPath(current)
344                             .replace("/", ".")
345                             .replace(File.separator, ".");
346 
347                     if (transmutedCanonicalPath.contains(currentClass.getPackage().getName())) {
348                         toReturn.add(current);
349                     }
350                 }
351             }
352         }
353 
354         // All done.
355         return toReturn;
356     }
357 
358     /**
359      * Prints the content of the supplied DOM Document as a string.
360      *
361      * @param doc A non-null DOM Document.
362      * @return A String holding the pretty-printed version of the supplied doc.
363      */
364     public static String printDocument(final Document doc) {
365 
366         try {
367             // Create the Unity-Transformer
368             final TransformerFactory tf = TransformerFactory.newInstance();
369             final Transformer transformer = tf.newTransformer();
370 
371             // Make it pretty print stuff.
372             transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
373             transformer.setOutputProperty(OutputKeys.METHOD, "xml");
374             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
375             transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
376             transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
377 
378             // Harvest the result, and return.
379             final StringWriter out = new StringWriter();
380             transformer.transform(new DOMSource(doc), new StreamResult(out));
381             return out.toString();
382         } catch (Exception e) {
383             throw new IllegalArgumentException("Could not print document", e);
384         }
385     }
386 }