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