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