View Javadoc
1   package org.codehaus.mojo.jaxb2.schemageneration;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.MojoExecutionException;
23  import org.apache.maven.plugin.logging.Log;
24  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.NodeProcessor;
25  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocRenderer;
26  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation;
27  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.XsdAnnotationProcessor;
28  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.XsdEnumerationAnnotationProcessor;
29  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.ChangeFilenameProcessor;
30  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.ChangeNamespacePrefixProcessor;
31  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.SimpleNamespaceResolver;
32  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.TransformSchema;
33  import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
34  import org.codehaus.mojo.jaxb2.shared.Validate;
35  import org.codehaus.plexus.util.FileUtils;
36  import org.codehaus.plexus.util.IOUtil;
37  import org.codehaus.plexus.util.StringUtils;
38  import org.w3c.dom.Document;
39  import org.w3c.dom.NamedNodeMap;
40  import org.w3c.dom.Node;
41  import org.w3c.dom.NodeList;
42  import org.xml.sax.InputSource;
43  
44  import javax.xml.parsers.DocumentBuilderFactory;
45  import javax.xml.transform.OutputKeys;
46  import javax.xml.transform.Transformer;
47  import javax.xml.transform.TransformerException;
48  import javax.xml.transform.TransformerFactory;
49  import javax.xml.transform.dom.DOMSource;
50  import javax.xml.transform.stream.StreamResult;
51  import java.io.BufferedWriter;
52  import java.io.File;
53  import java.io.FileFilter;
54  import java.io.FileNotFoundException;
55  import java.io.FileReader;
56  import java.io.FileWriter;
57  import java.io.IOException;
58  import java.io.Reader;
59  import java.io.StringWriter;
60  import java.io.Writer;
61  import java.util.ArrayList;
62  import java.util.Arrays;
63  import java.util.List;
64  import java.util.Map;
65  import java.util.TreeMap;
66  
67  /**
68   * Utility class holding algorithms used when generating XSD schema.
69   *
70   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>
71   * @since 1.4
72   */
73  public final class XsdGeneratorHelper {
74  
75      // Constants
76      private static final String MISCONFIG = "Misconfiguration detected: ";
77      private static TransformerFactory FACTORY;
78      private static final FileFilter RECURSIVE_XSD_FILTER;
79  
80      /**
81       * Hide the constructor for utility classes.
82       */
83      private XsdGeneratorHelper() {
84          // Do nothing.
85      }
86  
87      static {
88  
89          // Create the static filter used for recursive generated XSD files detection.
90          RECURSIVE_XSD_FILTER = new FileFilter() {
91              @Override
92              public boolean accept(final File toMatch) {
93  
94                  if (toMatch.exists()) {
95  
96                      // Accept directories for recursive operation, and
97                      // files with names matching the SCHEMAGEN_EMITTED_FILENAME Pattern.
98                      return toMatch.isDirectory()
99                              || AbstractXsdGeneratorMojo.SCHEMAGEN_EMITTED_FILENAME.matcher(toMatch.getName()).matches();
100                 }
101 
102                 // Not a directory or XSD file.
103                 return false;
104             }
105         };
106     }
107 
108     /**
109      * Acquires a map relating generated schema filename to its SimpleNamespaceResolver.
110      *
111      * @param outputDirectory The output directory of the generated schema files.
112      * @return a map relating generated schema filename to an initialized SimpleNamespaceResolver.
113      * @throws MojoExecutionException if two generated schema files used the same namespace URI.
114      */
115     public static Map<String, SimpleNamespaceResolver> getFileNameToResolverMap(final File outputDirectory)
116             throws MojoExecutionException {
117 
118         final Map<String, SimpleNamespaceResolver> toReturn = new TreeMap<String, SimpleNamespaceResolver>();
119 
120         // Each generated schema file should be written to the output directory.
121         // Each generated schema file should have a unique targetNamespace.
122         File[] generatedSchemaFiles = outputDirectory.listFiles(new FileFilter() {
123             public boolean accept(File pathname) {
124                 return pathname.getName().startsWith("schema") && pathname.getName().endsWith(".xsd");
125             }
126         });
127 
128         for (File current : generatedSchemaFiles) {
129             toReturn.put(current.getName(), new SimpleNamespaceResolver(current));
130         }
131 
132         return toReturn;
133     }
134 
135     /**
136      * Validates that the list of Schemas provided within the configuration all contain unique values. Should a
137      * MojoExecutionException be thrown, it contains informative text about the exact nature of the configuration
138      * problem - we should simplify for all plugin users.
139      *
140      * @param configuredTransformSchemas The List of configuration schemas provided to this mojo.
141      * @throws MojoExecutionException if any two configuredSchemas instances contain duplicate values for any of the
142      *                                properties uri, prefix or file. Also throws a MojoExecutionException if the uri of any Schema is null
143      *                                or empty, or if none of the 'file' and 'prefix' properties are given within any of the
144      *                                configuredSchema instances.
145      */
146     public static void validateSchemasInPluginConfiguration(final List<TransformSchema> configuredTransformSchemas)
147             throws MojoExecutionException {
148 
149         final List<String> uris = new ArrayList<String>();
150         final List<String> prefixes = new ArrayList<String>();
151         final List<String> fileNames = new ArrayList<String>();
152 
153         for (int i = 0; i < configuredTransformSchemas.size(); i++) {
154             final TransformSchema current = configuredTransformSchemas.get(i);
155             final String currentURI = current.getUri();
156             final String currentPrefix = current.getToPrefix();
157             final String currentFile = current.getToFile();
158 
159             // We cannot work with a null or empty uri
160             if (StringUtils.isEmpty(currentURI)) {
161                 throw new MojoExecutionException(MISCONFIG + "Null or empty property 'uri' found in "
162                         + "plugin configuration for schema element at index [" + i + "]: " + current);
163             }
164 
165             // No point in having *only* a namespace.
166             if (StringUtils.isEmpty(currentPrefix) && StringUtils.isEmpty(currentFile)) {
167                 throw new MojoExecutionException(MISCONFIG + "Null or empty properties 'prefix' "
168                         + "and 'file' found within plugin configuration for schema element at index ["
169                         + i + "]: " + current);
170             }
171 
172             // Validate that all given uris are unique.
173             if (uris.contains(currentURI)) {
174                 throw new MojoExecutionException(getDuplicationErrorMessage("uri", currentURI,
175                         uris.indexOf(currentURI), i));
176             }
177             uris.add(currentURI);
178 
179             // Validate that all given prefixes are unique.
180             if (prefixes.contains(currentPrefix) && !(currentPrefix == null)) {
181                 throw new MojoExecutionException(getDuplicationErrorMessage("prefix", currentPrefix,
182                         prefixes.indexOf(currentPrefix), i));
183             }
184             prefixes.add(currentPrefix);
185 
186             // Validate that all given files are unique.
187             if (fileNames.contains(currentFile)) {
188                 throw new MojoExecutionException(getDuplicationErrorMessage("file", currentFile,
189                         fileNames.indexOf(currentFile), i));
190             }
191             fileNames.add(currentFile);
192         }
193     }
194 
195     /**
196      * Inserts XML documentation annotations into all generated XSD files found
197      * within the supplied outputDir.
198      *
199      * @param log       A Maven Log.
200      * @param outputDir The outputDir, where generated XSD files are found.
201      * @param docs      The SearchableDocumentation for the source files within the compilation unit.
202      * @param renderer  The JavaDocRenderer used to convert JavaDoc annotations into XML documentation annotations.
203      * @return The number of processed XSDs.
204      */
205     public static int insertJavaDocAsAnnotations(final Log log,
206             final File outputDir,
207             final SearchableDocumentation docs,
208             final JavaDocRenderer renderer) {
209 
210         // Check sanity
211         Validate.notNull(docs, "docs");
212         Validate.notNull(log, "log");
213         Validate.notNull(outputDir, "outputDir");
214         Validate.isTrue(outputDir.isDirectory(), "'outputDir' must be a Directory.");
215         Validate.notNull(renderer, "renderer");
216 
217         int processedXSDs = 0;
218         final List<File> foundFiles = new ArrayList<File>();
219         addRecursively(foundFiles, RECURSIVE_XSD_FILTER, outputDir);
220 
221         if (foundFiles.size() > 0) {
222 
223             // Create the processors.
224             final XsdAnnotationProcessor classProcessor = new XsdAnnotationProcessor(docs, renderer);
225             final XsdEnumerationAnnotationProcessor enumProcessor
226                     = new XsdEnumerationAnnotationProcessor(docs, renderer);
227 
228             for (File current : foundFiles) {
229 
230                 // Create an XSD document from the current File.
231                 final Document generatedSchemaFileDocument = parseXmlToDocument(current);
232 
233                 // Replace all namespace prefixes within the provided document.
234                 process(generatedSchemaFileDocument.getFirstChild(), true, classProcessor);
235                 processedXSDs++;
236 
237                 // Overwrite the vanilla file.
238                 savePrettyPrintedDocument(generatedSchemaFileDocument, current);
239             }
240 
241         } else {
242             if (log.isWarnEnabled()) {
243                 log.warn("Found no generated 'vanilla' XSD files to process under ["
244                         + FileSystemUtilities.getCanonicalPath(outputDir) + "]. Aborting processing.");
245             }
246         }
247 
248         // All done.
249         return processedXSDs;
250     }
251 
252     /**
253      * Replaces all namespaces within generated schema files, as instructed by the configured Schema instances.
254      *
255      * @param resolverMap                The map relating generated schema file name to SimpleNamespaceResolver instances.
256      * @param configuredTransformSchemas The Schema instances read from the configuration of this plugin.
257      * @param mavenLog                   The active Log.
258      * @param schemaDirectory            The directory where all generated schema files reside.
259      * @throws MojoExecutionException If the namespace replacement could not be done.
260      */
261     public static void replaceNamespacePrefixes(final Map<String, SimpleNamespaceResolver> resolverMap,
262             final List<TransformSchema> configuredTransformSchemas,
263             final Log mavenLog,
264             final File schemaDirectory) throws MojoExecutionException {
265 
266         if (mavenLog.isDebugEnabled()) {
267             mavenLog.debug("Got resolverMap.keySet() [generated filenames]: " + resolverMap.keySet());
268         }
269 
270         for (SimpleNamespaceResolver currentResolver : resolverMap.values()) {
271             File generatedSchemaFile = new File(schemaDirectory, currentResolver.getSourceFilename());
272             Document generatedSchemaFileDocument = null;
273 
274             for (TransformSchema currentTransformSchema : configuredTransformSchemas) {
275                 // Should we alter the namespace prefix as instructed by the current schema?
276                 final String newPrefix = currentTransformSchema.getToPrefix();
277                 final String currentUri = currentTransformSchema.getUri();
278 
279                 if (StringUtils.isNotEmpty(newPrefix)) {
280                     // Find the old/current prefix of the namespace for the current schema uri.
281                     final String oldPrefix = currentResolver.getNamespaceURI2PrefixMap().get(currentUri);
282 
283                     if (StringUtils.isNotEmpty(oldPrefix)) {
284                         // Can we perform the prefix substitution?
285                         validatePrefixSubstitutionIsPossible(oldPrefix, newPrefix, currentResolver);
286 
287                         if (mavenLog.isDebugEnabled()) {
288                             mavenLog.debug("Subtituting namespace prefix [" + oldPrefix + "] with [" + newPrefix
289                                     + "] in file [" + currentResolver.getSourceFilename() + "].");
290                         }
291 
292                         // Get the Document of the current schema file.
293                         if (generatedSchemaFileDocument == null) {
294                             generatedSchemaFileDocument = parseXmlToDocument(generatedSchemaFile);
295                         }
296 
297                         // Replace all namespace prefixes within the provided document.
298                         process(generatedSchemaFileDocument.getFirstChild(), true,
299                                 new ChangeNamespacePrefixProcessor(oldPrefix, newPrefix));
300                     }
301                 }
302             }
303 
304             if (generatedSchemaFileDocument != null) {
305                 // Overwrite the generatedSchemaFile with the content of the generatedSchemaFileDocument.
306                 mavenLog.debug("Overwriting file [" + currentResolver.getSourceFilename() + "] with content ["
307                         + getHumanReadableXml(generatedSchemaFileDocument) + "]");
308                 savePrettyPrintedDocument(generatedSchemaFileDocument, generatedSchemaFile);
309             } else {
310                 mavenLog.debug("No namespace prefix changes to generated schema file ["
311                         + generatedSchemaFile.getName() + "]");
312             }
313         }
314     }
315 
316     /**
317      * Updates all schemaLocation attributes within the generated schema files to match the 'file' properties within the
318      * Schemas read from the plugin configuration. After that, the files are physically renamed.
319      *
320      * @param resolverMap                The map relating generated schema file name to SimpleNamespaceResolver instances.
321      * @param configuredTransformSchemas The Schema instances read from the configuration of this plugin.
322      * @param mavenLog                   The active Log.
323      * @param schemaDirectory            The directory where all generated schema files reside.
324      */
325     public static void renameGeneratedSchemaFiles(final Map<String, SimpleNamespaceResolver> resolverMap,
326             final List<TransformSchema> configuredTransformSchemas,
327             final Log mavenLog,
328             final File schemaDirectory) {
329 
330         // Create the map relating namespace URI to desired filenames.
331         Map<String, String> namespaceUriToDesiredFilenameMap = new TreeMap<String, String>();
332         for (TransformSchema current : configuredTransformSchemas) {
333             if (StringUtils.isNotEmpty(current.getToFile())) {
334                 namespaceUriToDesiredFilenameMap.put(current.getUri(), current.getToFile());
335             }
336         }
337 
338         // Replace the schemaLocation values to correspond to the new filenames
339         for (SimpleNamespaceResolver currentResolver : resolverMap.values()) {
340             File generatedSchemaFile = new File(schemaDirectory, currentResolver.getSourceFilename());
341             Document generatedSchemaFileDocument = parseXmlToDocument(generatedSchemaFile);
342 
343             // Replace all namespace prefixes within the provided document.
344             process(generatedSchemaFileDocument.getFirstChild(), true,
345                     new ChangeFilenameProcessor(namespaceUriToDesiredFilenameMap));
346 
347             // Overwrite the generatedSchemaFile with the content of the generatedSchemaFileDocument.
348             if (mavenLog.isDebugEnabled()) {
349                 mavenLog.debug("Changed schemaLocation entries within [" + currentResolver.getSourceFilename() + "]. "
350                         + "Result: [" + getHumanReadableXml(generatedSchemaFileDocument) + "]");
351             }
352             savePrettyPrintedDocument(generatedSchemaFileDocument, generatedSchemaFile);
353         }
354 
355         // Now, rename the actual files.
356         for (SimpleNamespaceResolver currentResolver : resolverMap.values()) {
357             final String localNamespaceURI = currentResolver.getLocalNamespaceURI();
358 
359             if (StringUtils.isEmpty(localNamespaceURI)) {
360                 mavenLog.warn("SimpleNamespaceResolver contained no localNamespaceURI; aborting rename.");
361                 continue;
362             }
363 
364             final String newFilename = namespaceUriToDesiredFilenameMap.get(localNamespaceURI);
365             final File originalFile = new File(schemaDirectory, currentResolver.getSourceFilename());
366 
367             if (StringUtils.isNotEmpty(newFilename)) {
368                 File renamedFile = FileUtils.resolveFile(schemaDirectory, newFilename);
369                 String renameResult = (originalFile.renameTo(renamedFile) ? "Success " : "Failure ");
370 
371                 if (mavenLog.isDebugEnabled()) {
372                     String suffix = "renaming [" + originalFile.getAbsolutePath() + "] to [" + renamedFile + "]";
373                     mavenLog.debug(renameResult + suffix);
374                 }
375             }
376         }
377     }
378 
379     /**
380      * Drives the supplied visitor to process the provided Node and all its children, should the recurseToChildren flag
381      * be set to <code>true</code>. All attributes of the current node are processed before recursing to children (i.e.
382      * breadth first recursion).
383      *
384      * @param node              The Node to process.
385      * @param recurseToChildren if <code>true</code>, processes all children of the supplied node recursively.
386      * @param visitor           The NodeProcessor instance which should process the nodes.
387      */
388     public static void process(final Node node, final boolean recurseToChildren, final NodeProcessor visitor) {
389 
390         // Process the current Node, if the NodeProcessor accepts it.
391         if (visitor.accept(node)) {
392             visitor.process(node);
393         }
394 
395         NamedNodeMap attributes = node.getAttributes();
396         for (int i = 0; i < attributes.getLength(); i++) {
397             Node attribute = attributes.item(i);
398 
399             // Process the current attribute, if the NodeProcessor accepts it.
400             if (visitor.accept(attribute)) {
401                 visitor.process(attribute);
402             }
403         }
404 
405         if (recurseToChildren) {
406             NodeList children = node.getChildNodes();
407             for (int i = 0; i < children.getLength(); i++) {
408                 Node child = children.item(i);
409 
410                 // Recurse to Element children.
411                 if (child.getNodeType() == Node.ELEMENT_NODE) {
412                     process(child, true, visitor);
413                 }
414             }
415         }
416     }
417 
418     /**
419      * Parses the provided InputStream to create a dom Document.
420      *
421      * @param xmlStream An InputStream connected to an XML document.
422      * @return A DOM Document created from the contents of the provided stream.
423      */
424     public static Document parseXmlStream(final Reader xmlStream) {
425 
426         // Build a DOM model of the provided xmlFileStream.
427         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
428         factory.setNamespaceAware(true);
429 
430         try {
431             return factory.newDocumentBuilder().parse(new InputSource(xmlStream));
432         } catch (Exception e) {
433             throw new IllegalArgumentException("Could not acquire DOM Document", e);
434         }
435     }
436 
437     /**
438      * Converts the provided DOM Node to a pretty-printed XML-formatted string.
439      *
440      * @param node The Node whose children should be converted to a String.
441      * @return a pretty-printed XML-formatted string.
442      */
443     protected static String getHumanReadableXml(final Node node) {
444         StringWriter toReturn = new StringWriter();
445 
446         try {
447             Transformer transformer = getFactory().newTransformer();
448             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
449             transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
450             transformer.transform(new DOMSource(node), new StreamResult(toReturn));
451         } catch (TransformerException e) {
452             throw new IllegalStateException("Could not transform node [" + node.getNodeName() + "] to XML", e);
453         }
454 
455         return toReturn.toString();
456     }
457 
458     //
459     // Private helpers
460     //
461 
462     private static String getDuplicationErrorMessage(final String propertyName, final String propertyValue,
463             final int firstIndex, final int currentIndex) {
464         return MISCONFIG + "Duplicate '" + propertyName + "' property with value [" + propertyValue
465                 + "] found in plugin configuration. Correct schema elements index (" + firstIndex + ") and ("
466                 + currentIndex + "), to ensure that all '" + propertyName + "' values are unique.";
467     }
468 
469     /**
470      * Validates that the transformation from <code>oldPrefix</code> to <code>newPrefix</code> is possible, in that
471      * <code>newPrefix</code> is not already used by a schema file. This would corrupt the schema by assigning elements
472      * from one namespace to another.
473      *
474      * @param oldPrefix       The old/current namespace prefix.
475      * @param newPrefix       The new/future namespace prefix.
476      * @param currentResolver The currently active SimpleNamespaceResolver.
477      * @throws MojoExecutionException if any schema file currently uses <code>newPrefix</code>.
478      */
479     private static void validatePrefixSubstitutionIsPossible(final String oldPrefix, final String newPrefix,
480             final SimpleNamespaceResolver currentResolver)
481             throws MojoExecutionException {
482         // Make certain the newPrefix does not exist already.
483         if (currentResolver.getNamespaceURI2PrefixMap().containsValue(newPrefix)) {
484             throw new MojoExecutionException(MISCONFIG + "Namespace prefix [" + newPrefix + "] is already in use."
485                     + " Cannot replace namespace prefix [" + oldPrefix + "] with [" + newPrefix + "] in file ["
486                     + currentResolver.getSourceFilename() + "].");
487         }
488     }
489 
490     /**
491      * Creates a Document from parsing the XML within the provided xmlFile.
492      *
493      * @param xmlFile The XML file to be parsed.
494      * @return The Document corresponding to the xmlFile.
495      */
496     private static Document parseXmlToDocument(final File xmlFile) {
497         Document result = null;
498         Reader reader = null;
499         try {
500             reader = new FileReader(xmlFile);
501             result = parseXmlStream(reader);
502         } catch (FileNotFoundException e) {
503             // This should never happen...
504         } finally {
505             IOUtil.close(reader);
506         }
507 
508         return result;
509     }
510 
511     private static void savePrettyPrintedDocument(final Document toSave, final File targetFile) {
512         Writer out = null;
513         try {
514             out = new BufferedWriter(new FileWriter(targetFile));
515             out.write(getHumanReadableXml(toSave.getFirstChild()));
516         } catch (IOException e) {
517             throw new IllegalStateException("Could not write to file [" + targetFile.getAbsolutePath() + "]", e);
518         } finally {
519             IOUtil.close(out);
520         }
521     }
522 
523     private static void addRecursively(final List<File> toPopulate,
524             final FileFilter fileFilter,
525             final File aDir) {
526 
527         // Check sanity
528         Validate.notNull(toPopulate, "toPopulate");
529         Validate.notNull(fileFilter, "fileFilter");
530         Validate.notNull(aDir, "aDir");
531 
532         // Add all matching files.
533         for (File current : aDir.listFiles(fileFilter)) {
534 
535             if (current.isFile()) {
536                 toPopulate.add(current);
537             } else if (current.isDirectory()) {
538                 addRecursively(toPopulate, fileFilter, current);
539             }
540         }
541     }
542 
543     private static TransformerFactory getFactory() {
544 
545         if (FACTORY == null) {
546 
547             try {
548                 FACTORY = TransformerFactory.newInstance();
549 
550                 // Harmonize XML formatting
551                 for (String currentAttributeName : Arrays.asList("indent-number", OutputKeys.INDENT)) {
552                     try {
553                         FACTORY.setAttribute(currentAttributeName, 2);
554                     } catch (IllegalArgumentException ex) {
555                         // Ignore this.
556                     }
557                 }
558             } catch (Throwable exception) {
559 
560                 // This should really not happen... but it seems to happen in some test cases.
561                 throw new IllegalStateException("Could not acquire TransformerFactory implementation.", exception);
562             }
563         }
564 
565         // All done.
566         return FACTORY;
567     }
568 }