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