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 com.sun.tools.jxc.SchemaGenerator;
23  import com.thoughtworks.qdox.JavaProjectBuilder;
24  import com.thoughtworks.qdox.model.JavaClass;
25  import com.thoughtworks.qdox.model.JavaPackage;
26  import com.thoughtworks.qdox.model.JavaSource;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugin.MojoFailureException;
29  import org.apache.maven.plugins.annotations.Parameter;
30  import org.codehaus.mojo.jaxb2.AbstractJaxbMojo;
31  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.DefaultJavaDocRenderer;
32  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocExtractor;
33  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocRenderer;
34  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation;
35  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.SimpleNamespaceResolver;
36  import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.TransformSchema;
37  import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
38  import org.codehaus.mojo.jaxb2.shared.arguments.ArgumentBuilder;
39  import org.codehaus.mojo.jaxb2.shared.environment.EnvironmentFacet;
40  import org.codehaus.mojo.jaxb2.shared.environment.ToolExecutionEnvironment;
41  import org.codehaus.mojo.jaxb2.shared.environment.classloading.ThreadContextClassLoaderBuilder;
42  import org.codehaus.mojo.jaxb2.shared.environment.locale.LocaleFacet;
43  import org.codehaus.mojo.jaxb2.shared.environment.logging.LoggingHandlerEnvironmentFacet;
44  import org.codehaus.mojo.jaxb2.shared.filters.Filter;
45  import org.codehaus.mojo.jaxb2.shared.filters.pattern.PatternFileFilter;
46  import org.codehaus.plexus.classworlds.realm.ClassRealm;
47  import org.codehaus.plexus.util.FileUtils;
48  
49  import javax.tools.ToolProvider;
50  import java.io.File;
51  import java.io.IOException;
52  import java.net.HttpURLConnection;
53  import java.net.URL;
54  import java.net.URLConnection;
55  import java.util.ArrayList;
56  import java.util.Arrays;
57  import java.util.Collection;
58  import java.util.Collections;
59  import java.util.List;
60  import java.util.Map;
61  import java.util.SortedMap;
62  import java.util.TreeMap;
63  import java.util.regex.Pattern;
64  
65  /**
66   * <p>Abstract superclass for Mojos that generate XSD files from annotated Java Sources.
67   * This Mojo delegates execution to the {@code schemagen} tool to perform the XSD file
68   * generation. Moreover, the AbstractXsdGeneratorMojo provides an augmented processing
69   * pipeline by optionally letting a set of NodeProcessors improve the 'vanilla' XSD files.</p>
70   *
71   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>
72   * @see <a href="https://jaxb.java.net/">The JAXB Reference Implementation</a>
73   */
74  public abstract class AbstractXsdGeneratorMojo extends AbstractJaxbMojo {
75  
76      /**
77       * <p>Pattern matching the names of files emitted by the JAXB/JDK SchemaGenerator.
78       * According to the JAXB Schema Generator documentation:</p>
79       * <blockquote>There is no way to control the name of the generated schema files at this time.</blockquote>
80       */
81      public static final Pattern SCHEMAGEN_EMITTED_FILENAME = Pattern.compile("schema\\p{javaDigit}+.xsd");
82  
83      /**
84       * <p>The default JavaDocRenderer used unless another JavaDocRenderer should be used.</p>
85       *
86       * @see #javaDocRenderer
87       * @since 2.0
88       */
89      public static final JavaDocRenderer STANDARD_JAVADOC_RENDERER = new DefaultJavaDocRenderer();
90  
91      /**
92       * <p>Default exclude file name suffixes for testSources, used unless overridden by an
93       * explicit configuration in the {@code testSourceExcludeSuffixes} parameter.
94       * </p>
95       * <pre>
96       *     <code>
97       *         final List&lt;Filter&lt;File&gt;&gt; schemagenTmp = new ArrayList&lt;Filter&lt;File&gt;&gt;();
98       *         schemagenTmp.addAll(AbstractJaxbMojo.STANDARD_EXCLUDE_FILTERS);
99       *         schemagenTmp.add(new PatternFileFilter(Arrays.asList("\\.java", "\\.scala", "\\.mdo"), false));
100      *         STANDARD_BYTECODE_EXCLUDE_FILTERS = Collections.unmodifiableList(schemagenTmp);
101      *     </code>
102      * </pre>
103      */
104     public static final List<Filter<File>> STANDARD_BYTECODE_EXCLUDE_FILTERS;
105 
106     /**
107      * Filter list containing a PatternFileFilter including ".class" files.
108      */
109     public static final List<Filter<File>> CLASS_INCLUDE_FILTERS;
110 
111     /**
112      * Specification for packages which must be loaded using the SystemToolClassLoader (and not in the plugin's
113      * ThreadContext ClassLoader). The SystemToolClassLoader is used by SchemaGen to process some stuff from the
114      * {@code tools.jar} archive, in particular its exception types used to signal JAXB annotation Exceptions.
115      *
116      * @see ToolProvider#getSystemToolClassLoader()
117      */
118     public static final List<String> SYSTEM_TOOLS_CLASSLOADER_PACKAGES = Arrays.asList(
119             "com.sun.source.util",
120             "com.sun.source.tree");
121 
122     static {
123 
124         final List<Filter<File>> schemagenTmp = new ArrayList<Filter<File>>();
125         schemagenTmp.addAll(AbstractJaxbMojo.STANDARD_EXCLUDE_FILTERS);
126         schemagenTmp.add(new PatternFileFilter(Arrays.asList("\\.java", "\\.scala", "\\.mdo"), false));
127         STANDARD_BYTECODE_EXCLUDE_FILTERS = Collections.unmodifiableList(schemagenTmp);
128 
129         CLASS_INCLUDE_FILTERS = new ArrayList<Filter<File>>();
130         CLASS_INCLUDE_FILTERS.add(new PatternFileFilter(Arrays.asList("\\.class"), true));
131     }
132 
133     // Internal state
134     private static final int SCHEMAGEN_INCORRECT_OPTIONS = -1;
135     private static final int SCHEMAGEN_COMPLETED_OK = 0;
136     private static final int SCHEMAGEN_JAXB_ERRORS = 1;
137 
138     /**
139      * <p>A List holding desired schema mappings, each of which binds a schema namespace URI to its desired prefix
140      * [optional] and the name of the resulting schema file [optional]. All given elements (uri, prefix, file) must be
141      * unique within the configuration; no two elements may have the same values.</p>
142      * <p>The example schema configuration below maps two namespace uris to prefixes and generated file names. This implies
143      * that <tt>http://some/namespace</tt> will be represented by the prefix <tt>some</tt> within the generated XML
144      * Schema files; creating namespace definitions on the form <tt>xmlns:some="http://some/namespace"</tt>, and
145      * corresponding uses on the form <tt>&lt;xs:element minOccurs="0"
146      * ref="<strong>some:</strong>anOptionalElementInSomeNamespace"/></tt>. Moreover, the file element defines that the
147      * <tt>http://some/namespace</tt> definitions will be written to the file <tt>some_schema.xsd</tt>, and that all
148      * import references will be on the form <tt>&lt;xs:import namespace="http://some/namespace"
149      * schemaLocation="<strong>some_schema.xsd</strong>"/></tt></p>
150      * <p>The example configuration below also performs identical operations for the namespace uri
151      * <tt>http://another/namespace</tt> with the prefix <tt>another</tt> and the file <tt>another_schema.xsd</tt>.
152      * </p>
153      * <pre>
154      *     <code>
155      * &lt;transformSchemas>
156      *   &lt;transformSchema>
157      *     &lt;uri>http://some/namespace&lt;/uri>;
158      *     &lt;toPrefix>some&lt;/toPrefix>
159      *     &lt;toFile>some_schema.xsd&lt;/toFile>
160      *   &lt;transformSchema>
161      *     &lt;uri>http://another/namespace&lt;/uri>;
162      *     &lt;toPrefix>another&lt;/toPrefix>
163      *     &lt;toFile>another_schema.xsd&lt;/toFile>
164      *   &lt;/transformSchema>
165      * &lt;/transformSchemas>
166      *     </code>
167      * </pre>
168      *
169      * @since 1.4
170      */
171     @Parameter
172     private List<TransformSchema> transformSchemas;
173 
174     /**
175      * <strong>Deprecated - will be removed in a future release</strong>
176      * <p>From plugin version 2.4, this parameter will not be used.
177      * Instead, episode files are generated by default with all JAXB operations.</p>
178      * <p>Starting with plugin version 2.4, use the parameter {@link #episodeFileName} to provide a custom
179      * name of the generated episode File (or rely on the standard file name {@link #STANDARD_EPISODE_FILENAME}).</p>
180      *
181      * @since 2.0
182      * @deprecated
183      */
184     @Deprecated
185     @Parameter(defaultValue = "true")
186     protected boolean generateEpisode;
187 
188     /**
189      * <p>Corresponding SchemaGen parameter: {@code episode}.</p>
190      * <p>Generate an episode file with the supplied name from this XSD generation, so that other schemas that rely
191      * on this schema can be compiled later and rely on classes that are generated from this compilation.
192      * The generated episode file is simply a JAXB customization file (but with vendor extensions), normally known
193      * as a <em>binding file</em> with the suffix <code>.xjb</code>.</p>
194      * <p>If the <code>episodeFileName</code> parameter is not given, the episode file name is synthesized on the form
195      * <code>"episode_" + executionID + ".xjb"</code> - typically something like <em>episode_schemagen.xjb</em>, but
196      * it depends on the actual ID given in the execution element:</p>
197      * <pre>
198      *     <code>
199      * &lt;executions&gt;
200      *     &lt;execution&gt;
201      *         &lt;id&gt;schemagen&lt;/id&gt;
202      *         &lt;goals&gt;
203      *             &lt;goal&gt;schemagen&lt;/goal&gt;
204      *         &lt;/goals&gt;
205      *     &lt;/execution&gt;
206      * &lt;/executions&gt;
207      *      </code>
208      * </pre>
209      *
210      * @see #STANDARD_EPISODE_FILENAME
211      * @since 2.4
212      */
213     @Parameter
214     protected String episodeFileName;
215 
216     /**
217      * <p>If {@code true}, Elements or Attributes in the generated XSD files will be annotated with any
218      * JavaDoc found for their respective properties. If {@code false}, no XML documentation annotations will be
219      * generated in post-processing any results from the JAXB SchemaGenerator.</p>
220      *
221      * @since 2.0
222      */
223     @Parameter(defaultValue = "true")
224     protected boolean createJavaDocAnnotations;
225 
226     /**
227      * <p>A renderer used to create XML annotation text from JavaDoc comments found within the source code.
228      * Unless another implementation is provided, the standard JavaDocRenderer used is
229      * {@linkplain org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.DefaultJavaDocRenderer}.</p>
230      *
231      * @see org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.DefaultJavaDocRenderer
232      * @since 2.0
233      */
234     @Parameter
235     protected JavaDocRenderer javaDocRenderer;
236 
237     /**
238      * <p>Removes all files from the output directory before running SchemaGenerator.</p>
239      *
240      * @since 2.0
241      */
242     @Parameter(defaultValue = "true")
243     protected boolean clearOutputDir;
244 
245     /**
246      * <p>XSD schema files are not generated from POM projects or if no includes have been supplied.</p>
247      * {@inheritDoc}
248      */
249     @Override
250     protected boolean shouldExecutionBeSkipped() {
251 
252         boolean toReturn = false;
253 
254         if ("pom".equalsIgnoreCase(getProject().getPackaging())) {
255             warnAboutIncorrectPluginConfiguration("packaging", "POM-packaged projects should not generate XSDs.");
256             toReturn = true;
257         }
258 
259         if (getSources().isEmpty()) {
260             warnAboutIncorrectPluginConfiguration("sources", "At least one Java Source file has to be included.");
261             toReturn = true;
262         }
263 
264         // All done.
265         return toReturn;
266     }
267 
268     /**
269      * {@inheritDoc}
270      */
271     @Override
272     protected boolean isReGenerationRequired() {
273 
274         //
275         // Use the stale flag method to identify if we should re-generate the XSDs from the sources.
276         // Basically, we should re-generate the XSDs if:
277         //
278         // a) The staleFile does not exist
279         // b) The staleFile exists and is older than one of the sources (Java or XJB files).
280         //    "Older" is determined by comparing the modification timestamp of the staleFile and the source files.
281         //
282         final File staleFile = getStaleFile();
283         final String debugPrefix = "StaleFile [" + FileSystemUtilities.getCanonicalPath(staleFile) + "]";
284 
285         boolean stale = !staleFile.exists();
286         if (stale) {
287             getLog().debug(debugPrefix + " not found. XML Schema (re-)generation required.");
288         } else {
289 
290             final List<URL> sources = getSources();
291 
292             if (getLog().isDebugEnabled()) {
293                 getLog().debug(debugPrefix + " found. Checking timestamps on source Java "
294                         + "files to determine if XML Schema (re-)generation is required.");
295             }
296 
297             final long staleFileLastModified = staleFile.lastModified();
298             for (URL current : sources) {
299 
300                 final URLConnection sourceFileConnection;
301                 try {
302                     sourceFileConnection = current.openConnection();
303                     sourceFileConnection.connect();
304                 } catch (Exception e) {
305 
306                     if (getLog().isDebugEnabled()) {
307                         getLog().debug("Could not open a sourceFileConnection to [" + current + "]", e);
308                     }
309 
310                     // Can't determine if the staleFile is younger than this source.
311                     // Re-generate to be on the safe side.
312                     stale = true;
313                     break;
314                 }
315 
316                 try {
317                     if (sourceFileConnection.getLastModified() > staleFileLastModified) {
318 
319                         if (getLog().isDebugEnabled()) {
320                             getLog().debug(current.toString() + " is newer than the stale flag file.");
321                         }
322                         stale = true;
323                     }
324                 } finally {
325                     if (sourceFileConnection instanceof HttpURLConnection) {
326                         ((HttpURLConnection) sourceFileConnection).disconnect();
327                     }
328                 }
329             }
330         }
331 
332         // All done.
333         return stale;
334     }
335 
336     /**
337      * {@inheritDoc}
338      */
339     @Override
340     protected boolean performExecution() throws MojoExecutionException, MojoFailureException {
341 
342         boolean updateStaleFileTimestamp = false;
343         ToolExecutionEnvironment environment = null;
344 
345         try {
346 
347             //
348             // Ensure that classes that SchemaGen expects to be loaded in the SystemToolClassLoader
349             // is delegated to that ClassLoader, to comply with SchemaGen's internal reflective loading
350             // of classes. Otherwise we will have ClassCastExceptions instead of proper execution.
351             //
352             final ClassRealm localRealm = (ClassRealm) getClass().getClassLoader();
353             for (String current : SYSTEM_TOOLS_CLASSLOADER_PACKAGES) {
354                 localRealm.importFrom(ToolProvider.getSystemToolClassLoader(), current);
355             }
356 
357             // Configure the ThreadContextClassLoaderBuilder, to enable synthesizing a correct ClassPath for the tool.
358             final ThreadContextClassLoaderBuilder classLoaderBuilder = ThreadContextClassLoaderBuilder
359                     .createFor(this.getClass(), getLog(), getEncoding(false))
360                     .addPaths(getClasspath())
361                     .addPaths(getProject().getCompileSourceRoots());
362 
363             final LocaleFacet localeFacet = locale == null ? null : LocaleFacet.createFor(locale, getLog());
364 
365             // Create the execution environment as required by the XJC tool.
366             environment = new ToolExecutionEnvironment(
367                     getLog(),
368                     classLoaderBuilder,
369                     LoggingHandlerEnvironmentFacet.create(getLog(), getClass(), getEncoding(false)),
370                     localeFacet);
371             final String projectBasedirPath = FileSystemUtilities.getCanonicalPath(getProject().getBasedir());
372 
373             // Add any extra configured EnvironmentFacets, as configured in the POM.
374             if (extraFacets != null) {
375                 for (EnvironmentFacet current : extraFacets) {
376                     environment.add(current);
377                 }
378             }
379 
380             // Setup the environment.
381             environment.setup();
382 
383             // Compile the SchemaGen arguments
384             final File episodeFile = getEpisodeFile(episodeFileName);
385             final List<URL> sources = getSources();
386             final String[] schemaGenArguments = getSchemaGenArguments(
387                     environment.getClassPathAsArgument(),
388                     episodeFile,
389                     sources);
390 
391             // Ensure that the outputDirectory and workDirectory exists.
392             // Clear them if configured to do so.
393             FileSystemUtilities.createDirectory(getOutputDirectory(), clearOutputDir);
394             FileSystemUtilities.createDirectory(getWorkDirectory(), clearOutputDir);
395 
396             // Re-generate the episode file's parent directory.
397             getEpisodeFile(episodeFileName);
398             // Do we need to re-create the episode file's parent directory?
399             /*final boolean reCreateEpisodeFileParentDirectory = generateEpisode && clearOutputDir;
400             if (reCreateEpisodeFileParentDirectory) {
401 
402             }
403             */
404 
405             try {
406 
407                 // Check the system properties.
408                 // logSystemPropertiesAndBasedir();
409 
410                 // Fire the SchemaGenerator
411                 final int result = SchemaGenerator.run(
412                         schemaGenArguments,
413                         Thread.currentThread().getContextClassLoader());
414 
415                 if (SCHEMAGEN_INCORRECT_OPTIONS == result) {
416                     printSchemaGenCommandAndThrowException(projectBasedirPath,
417                             sources,
418                             schemaGenArguments,
419                             result,
420                             null);
421                 } else if (SCHEMAGEN_JAXB_ERRORS == result) {
422 
423                     // TODO: Collect the error message(s) which was emitted by SchemaGen. How can this be done?
424                     throw new MojoExecutionException("JAXB errors arose while SchemaGen compiled sources to XML.");
425                 }
426 
427                 // Copy generated XSDs and episode files from the WorkDirectory to the OutputDirectory,
428                 // but do not copy the intermediary bytecode files generated by schemagen.
429                 final List<Filter<File>> exclusionFilters = PatternFileFilter.createIncludeFilterList(
430                         getLog(), "\\.class");
431 
432                 final List<File> toCopy = FileSystemUtilities.resolveRecursively(
433                         Arrays.asList(getWorkDirectory()),
434                         exclusionFilters, getLog());
435                 for (File current : toCopy) {
436 
437                     // Get the path to the current file
438                     final String currentPath = FileSystemUtilities.getCanonicalPath(current.getAbsoluteFile());
439                     final File target = new File(getOutputDirectory(),
440                             FileSystemUtilities.relativize(currentPath, getWorkDirectory(), true));
441 
442                     // Copy the file to the same relative structure within the output directory.
443                     FileSystemUtilities.createDirectory(target.getParentFile(), false);
444                     FileUtils.copyFile(current, target);
445                 }
446 
447                 //
448                 // The XSD post-processing should be applied in the following order:
449                 //
450                 // 1. [XsdAnnotationProcessor]:            Inject JavaDoc annotations for Classes.
451                 // 2. [XsdEnumerationAnnotationProcessor]: Inject JavaDoc annotations for Enums.
452                 // 3. [ChangeNamespacePrefixProcessor]:    Change namespace prefixes within XSDs.
453                 // 4. [ChangeFilenameProcessor]:           Change the fileNames of XSDs.
454                 //
455 
456                 final boolean performPostProcessing = createJavaDocAnnotations || transformSchemas != null;
457                 if (performPostProcessing) {
458 
459                     // Map the XML Namespaces to their respective XML URIs (and reverse)
460                     // The keys are the generated 'vanilla' XSD file names.
461                     final Map<String, SimpleNamespaceResolver> resolverMap =
462                             XsdGeneratorHelper.getFileNameToResolverMap(getOutputDirectory());
463 
464                     if (createJavaDocAnnotations) {
465 
466                         if (getLog().isInfoEnabled()) {
467                             getLog().info("XSD post-processing: Adding JavaDoc annotations in generated XSDs.");
468                         }
469 
470                         // Resolve the sources
471                         final List<File> fileSources = new ArrayList<File>();
472                         for (URL current : sources) {
473                             if ("file".equalsIgnoreCase(current.getProtocol())) {
474                                 final File toAdd = new File(current.getPath());
475                                 if (toAdd.exists()) {
476                                     fileSources.add(toAdd);
477                                 } else {
478                                     if (getLog().isWarnEnabled()) {
479                                         getLog().warn("Ignoring URL [" + current + "] as it is a nonexistent file.");
480                                     }
481                                 }
482                             }
483                         }
484 
485                         final List<File> files = FileSystemUtilities.resolveRecursively(
486                                 fileSources, null, getLog());
487 
488                         // Acquire JavaDocs
489                         final JavaDocExtractor extractor = new JavaDocExtractor(getLog()).addSourceFiles(files);
490                         final SearchableDocumentation javaDocs = extractor.process();
491 
492                         // Modify the 'vanilla' generated XSDs by inserting the JavaDoc as annotations
493                         final JavaDocRenderer renderer = javaDocRenderer == null
494                                 ? STANDARD_JAVADOC_RENDERER
495                                 : javaDocRenderer;
496                         final int numProcessedFiles = XsdGeneratorHelper.insertJavaDocAsAnnotations(getLog(),
497                                 getEncoding(false),
498                                 getOutputDirectory(),
499                                 javaDocs,
500                                 renderer);
501 
502                         if (getLog().isDebugEnabled()) {
503                             getLog().info("XSD post-processing: " + numProcessedFiles + " files processed.");
504                         }
505                     }
506 
507                     if (transformSchemas != null) {
508 
509                         if (getLog().isInfoEnabled()) {
510                             getLog().info("XSD post-processing: Renaming and converting XSDs.");
511                         }
512 
513                         // Transform all namespace prefixes as requested.
514                         XsdGeneratorHelper.replaceNamespacePrefixes(resolverMap,
515                                 transformSchemas,
516                                 getLog(),
517                                 getOutputDirectory(),
518                                 getEncoding(false));
519 
520                         // Rename all generated schema files as requested.
521                         XsdGeneratorHelper.renameGeneratedSchemaFiles(resolverMap,
522                                 transformSchemas,
523                                 getLog(),
524                                 getOutputDirectory(),
525                                 getEncoding(false));
526                     }
527                 }
528 
529             } catch (MojoExecutionException e) {
530                 throw e;
531             } catch (Exception e) {
532 
533                 // Find the root exception, and print its stack trace to the Maven Log.
534                 // These invocation target exceptions tend to produce really deep stack traces,
535                 // hiding the actual root cause of the exception.
536                 Throwable current = e;
537                 while (current.getCause() != null) {
538                     current = current.getCause();
539                 }
540 
541                 getLog().error("Execution failed.");
542 
543                 //
544                 // Print a stack trace
545                 //
546                 StringBuilder rootCauseBuilder = new StringBuilder();
547                 rootCauseBuilder.append("\n");
548                 rootCauseBuilder.append("[Exception]: " + current.getClass().getName() + "\n");
549                 rootCauseBuilder.append("[Message]: " + current.getMessage() + "\n");
550                 for (StackTraceElement el : current.getStackTrace()) {
551                     rootCauseBuilder.append("         " + el.toString()).append("\n");
552                 }
553                 getLog().error(rootCauseBuilder.toString().replaceAll("[\r\n]+", "\n"));
554 
555                 printSchemaGenCommandAndThrowException(projectBasedirPath,
556                         sources,
557                         schemaGenArguments,
558                         -1,
559                         current);
560 
561             }
562 
563             // Indicate that the output directory was updated.
564             getBuildContext().refresh(getOutputDirectory());
565 
566             // Update the modification timestamp of the staleFile.
567             updateStaleFileTimestamp = true;
568 
569         } finally {
570 
571             // Restore the environment
572             if (environment != null) {
573                 environment.restore();
574             }
575         }
576 
577         // Add generated directories to
578 
579         // All done.
580         return updateStaleFileTimestamp;
581     }
582 
583     /**
584      * @return The working directory to which the SchemaGenerator should initially copy all its generated files,
585      * including bytecode files, compiled from java sources.
586      */
587     protected abstract File getWorkDirectory();
588 
589     /**
590      * Finds a List containing URLs to compiled bytecode files within this Compilation Unit.
591      * Typically this equals the resolved files under the project's build directories, plus any
592      * JAR artifacts found on the classpath.
593      *
594      * @return A non-null List containing URLs to bytecode files within this compilation unit.
595      * Typically this equals the resolved files under the project's build directories, plus any JAR
596      * artifacts found on the classpath.
597      */
598     protected abstract List<URL> getCompiledClassNames();
599 
600     /**
601      * Override this method to acquire a List holding all URLs to the SchemaGen Java sources for which this
602      * AbstractXsdGeneratorMojo should generate Xml Schema Descriptor files.
603      *
604      * @return A non-null List holding URLs to sources for the XSD generation.
605      */
606     @Override
607     protected abstract List<URL> getSources();
608 
609     //
610     // Private helpers
611     //
612 
613     private String[] getSchemaGenArguments(final String classPath,
614                                            final File episodeFile,
615                                            final List<URL> sources)
616             throws MojoExecutionException {
617 
618         final ArgumentBuilder builder = new ArgumentBuilder();
619 
620         // Add all flags on the form '-flagName'
621         // builder.withFlag();
622 
623         // Add all arguments on the form '-argumentName argumentValue'
624         // (i.e. in 2 separate elements of the returned String[])
625         builder.withNamedArgument("encoding", getEncoding(true));
626         builder.withNamedArgument("d", getWorkDirectory().getAbsolutePath());
627         builder.withNamedArgument("classpath", classPath);
628 
629         // From 2.4: Always generate an episode file.
630         //
631         builder.withNamedArgument("episode", FileSystemUtilities.getCanonicalPath(episodeFile));
632 
633         try {
634 
635             //
636             // The SchemaGenerator does not support directories as arguments:
637             // "Caused by: java.lang.IllegalArgumentException: directories not supported"
638             // ... implying we must resolve source files in the compilation unit.
639             //
640             // There seems to be two ways of adding sources to the SchemaGen tool:
641             // 1) Using java source files
642             //    Define the relative paths to source files, calculated from the System.property "user.dir"
643             //    (i.e. *not* the Maven "basedir" property) on the form 'src/main/java/se/west/something/SomeClass.java'.
644             //    Sample: javac -d . ../github_jaxb2_plugin/src/it/schemagen-main/src/main/java/se/west/gnat/Foo.java
645             //
646             // 2) Using bytecode files
647             //    Define the CLASSPATH to point to build output directories (such as target/classes), and then use
648             //    package notation arguments on the form 'se.west.something.SomeClass'.
649             //    Sample: schemagen -d . -classpath brat se.west.gnat.Foo
650             //
651             // The jaxb2-maven-plugin uses these two methods in the order given.
652             //
653             builder.withPreCompiledArguments(getSchemaGeneratorSourceFiles(sources));
654         } catch (IOException e) {
655             throw new MojoExecutionException("Could not compile source paths for the SchemaGenerator", e);
656         }
657 
658         // All done.
659         return logAndReturnToolArguments(builder.build(), "SchemaGen");
660     }
661 
662     /**
663      * <p>The SchemaGenerator does not support directories as arguments, implying we must resolve source
664      * files in the compilation unit. This fact is shown when supplying a directory argument as source, when
665      * the tool emits:
666      * <blockquote>Caused by: java.lang.IllegalArgumentException: directories not supported</blockquote></p>
667      * <p>There seems to be two ways of adding sources to the SchemaGen tool:</p>
668      * <dl>
669      * <dt>1. <strong>Java Source</strong> files</dt>
670      * <dd>Define the relative paths to source files, calculated from the System.property {@code user.dir}
671      * (i.e. <strong>not</strong> the Maven {@code basedir} property) on the form
672      * {@code src/main/java/se/west/something/SomeClass.java}.<br/>
673      * <em>Sample</em>: {@code javac -d . .
674      * ./github_jaxb2_plugin/src/it/schemagen-main/src/main/java/se/west/gnat/Foo.java}</dd>
675      * <dt>2. <strong>Bytecode</strong> files</dt>
676      * <dd>Define the {@code CLASSPATH} to point to build output directories (such as target/classes), and then
677      * use package notation arguments on the form {@code se.west.something.SomeClass}.<br/>
678      * <em>Sample</em>: {@code schemagen -d . -classpath brat se.west.gnat.Foo}</dd>
679      * </dl>
680      * <p>The jaxb2-maven-plugin uses these two methods in the order given</p>
681      *
682      * @param sources The compiled sources (as calculated from the local project's
683      *                source paths, {@code getSources()}).
684      * @return A sorted List holding all sources to be used by the SchemaGenerator. According to the SchemaGenerator
685      * documentation, the order in which the source arguments are provided is irrelevant.
686      * The sources are to be rendered as the final (open-ended) argument to the schemagen execution.
687      * @see #getSources()
688      */
689     private List<String> getSchemaGeneratorSourceFiles(final List<URL> sources)
690             throws IOException, MojoExecutionException {
691 
692         final SortedMap<String, String> className2SourcePath = new TreeMap<String, String>();
693         final File baseDir = getProject().getBasedir();
694         final File userDir = new File(System.getProperty("user.dir"));
695         final String encoding = getEncoding(true);
696 
697         // 1) Find/add all sources available in the compilation unit.
698         for (URL current : sources) {
699 
700             final File sourceCodeFile = FileSystemUtilities.getFileFor(current, encoding);
701 
702             // Calculate the relative path for the current source
703             final String relativePath = FileSystemUtilities.relativize(
704                     FileSystemUtilities.getCanonicalPath(sourceCodeFile),
705                     userDir,
706                     true);
707 
708             if (getLog().isDebugEnabled()) {
709                 getLog().debug("SourceCodeFile ["
710                         + FileSystemUtilities.getCanonicalPath(sourceCodeFile)
711                         + "] and userDir [" + FileSystemUtilities.getCanonicalPath(userDir)
712                         + "] ==> relativePath: "
713                         + relativePath
714                         + ". (baseDir: " + FileSystemUtilities.getCanonicalPath(baseDir) + "]");
715             }
716 
717             // Find the Java class(es) within the source.
718             final JavaProjectBuilder builder = new JavaProjectBuilder();
719             builder.setEncoding(encoding);
720 
721             //
722             // Ensure that we include package-info.java classes in the SchemaGen compilation.
723             //
724             if (sourceCodeFile.getName().trim().equalsIgnoreCase(PACKAGE_INFO_FILENAME)) {
725 
726                 // For some reason, QDox requires the package-info.java to be added as a URL instead of a File.
727                 builder.addSource(current);
728                 final Collection<JavaPackage> packages = builder.getPackages();
729                 if (packages.size() != 1) {
730                     throw new MojoExecutionException("Exactly one package should be present in file ["
731                             + sourceCodeFile.getPath() + "]");
732                 }
733 
734                 // Make the key indicate that this is the package-info.java file.
735                 final JavaPackage javaPackage = packages.iterator().next();
736                 className2SourcePath.put("package-info for (" + javaPackage.getName() + ")", relativePath);
737                 continue;
738             }
739 
740             // This is not a package-info.java file, so QDox lets us add this as a File.
741             builder.addSource(sourceCodeFile);
742 
743             // Map any found FQCN to the relativized path of its source file.
744             for (JavaSource currentJavaSource : builder.getSources()) {
745                 for (JavaClass currentJavaClass : currentJavaSource.getClasses()) {
746 
747                     final String className = currentJavaClass.getFullyQualifiedName();
748                     if (className2SourcePath.containsKey(className)) {
749                         if (getLog().isWarnEnabled()) {
750                             getLog().warn("Already mapped. Source class [" + className + "] within ["
751                                     + className2SourcePath.get(className)
752                                     + "]. Not overwriting with [" + relativePath + "]");
753                         }
754                     } else {
755                         className2SourcePath.put(className, relativePath);
756                     }
757                 }
758             }
759         }
760 
761         /*
762         // 2) Find any bytecode available in the compilation unit, and add its file as a SchemaGen argument.
763         //
764         //    The algorithm is:
765         //    1) Add bytecode classpath unless its class is already added in source form.
766         //    2) SchemaGen cannot handle directory arguments, so any bytecode files in classpath directories
767         //       must be resolved.
768         //    3) All JARs in the classpath should be added as arguments to SchemaGen.
769         //
770         //    .... Gosh ...
771         //
772         for (URL current : getCompiledClassNames()) {
773             getLog().debug(" (compiled ClassName) --> " + current.toExternalForm());
774         }
775 
776         Filters.initialize(getLog(), CLASS_INCLUDE_FILTERS);
777 
778         final List<URL> classPathURLs = new ArrayList<URL>();
779         for (String current : getClasspath()) {
780 
781             final File currentFile = new File(current);
782             if (FileSystemUtilities.EXISTING_FILE.accept(currentFile)) {
783 
784                 // This is a file/JAR. Simply add its path to SchemaGen's arguments.
785                 classPathURLs.add(FileSystemUtilities.getUrlFor(currentFile));
786 
787             } else if (FileSystemUtilities.EXISTING_DIRECTORY.accept(currentFile)) {
788 
789                 // Resolve all bytecode files within this directory.
790                 // FileSystemUtilities.filterFiles(baseDir, )
791                 if (getLog().isDebugEnabled()) {
792                     getLog().debug("TODO: Resolve and add bytecode files within: ["
793                             + FileSystemUtilities.getCanonicalPath(currentFile) + "]");
794                 }
795 
796                 // Find the byte code files within the current directory.
797                 final List<File> byteCodeFiles = new ArrayList<File>();
798                 for(File currentResolvedFile : FileSystemUtilities.resolveRecursively(
799                         Arrays.asList(currentFile), null, getLog())) {
800 
801                     if(Filters.matchAtLeastOnce(currentResolvedFile, CLASS_INCLUDE_FILTERS)) {
802                         byteCodeFiles.add(currentResolvedFile);
803                     }
804                 }
805 
806                 for(File currentByteCodeFile : byteCodeFiles) {
807 
808                     final String currentCanonicalPath = FileSystemUtilities.getCanonicalPath(
809                             currentByteCodeFile.getAbsoluteFile());
810 
811                     final String relativized = FileSystemUtilities.relativize(currentCanonicalPath,
812                             FileSystemUtilities.getCanonicalFile(currentFile.getAbsoluteFile()));
813                     final String pathFromUserDir = FileSystemUtilities.relativize(currentCanonicalPath, userDir);
814 
815                     final String className = relativized.substring(0, relativized.indexOf(".class"))
816                             .replace("/", ".")
817                             .replace(File.separator, ".");
818 
819                     if(!className2SourcePath.containsKey(className)) {
820                         className2SourcePath.put(className, pathFromUserDir);
821 
822                         if(getLog().isDebugEnabled()) {
823                             getLog().debug("Adding ByteCode [" + className + "] at relativized path ["
824                                     + pathFromUserDir + "]");
825                         }
826                     } else {
827                         if(getLog().isDebugEnabled()) {
828                             getLog().debug("ByteCode [" + className + "] already added. Not re-adding.");
829                         }
830                     }
831                 }
832 
833             } else if (getLog().isWarnEnabled()) {
834 
835                 final String suffix = !currentFile.exists() ? " nonexistent" : " was neither a File nor a Directory";
836                 getLog().warn("Classpath part [" + current + "] " + suffix + ". Ignoring it.");
837             }
838         }
839 
840         /*
841         for (URL current : getCompiledClassNames()) {
842 
843             // TODO: FIX THIS!
844             // Get the class information data from the supplied URL
845             for (String currentClassPathElement : getClasspath()) {
846 
847                 if(getLog().isDebugEnabled()) {
848                     getLog().debug("Checking class path element: [" + currentClassPathElement + "]");
849                 }
850             }
851 
852             if(getLog().isDebugEnabled()) {
853                 getLog().debug("Processing compiledClassName: [" + current + "]");
854             }
855 
856             // Find the Java class(es) within the source.
857             final JavaProjectBuilder builder = new JavaProjectBuilder();
858             builder.setEncoding(getEncoding(true));
859             builder.addSource(current);
860 
861             for (JavaSource currentSource : builder.getSources()) {
862                 for (JavaClass currentClass : currentSource.getClasses()) {
863 
864                     final String className = currentClass.getFullyQualifiedName();
865                     if (className2SourcePath.containsKey(className)) {
866                         if (getLog().isWarnEnabled()) {
867                             getLog().warn("Already mapped. Source class [" + className + "] within ["
868                                     + className2SourcePath.get(className)
869                                     + "]. Not overwriting with [" + className + "]");
870                         }
871                     } else {
872                         className2SourcePath.put(className, className);
873                     }
874                 }
875             }
876         }
877         */
878 
879         if (getLog().isDebugEnabled()) {
880 
881             final int size = className2SourcePath.size();
882             getLog().debug("[ClassName-2-SourcePath Map (size: " + size + ")] ...");
883 
884             int i = 0;
885             for (Map.Entry<String, String> current : className2SourcePath.entrySet()) {
886                 getLog().debug("  " + (++i) + "/" + size + ": [" + current.getKey() + "]: "
887                         + current.getValue());
888             }
889             getLog().debug("... End [ClassName-2-SourcePath Map]");
890         }
891 
892         // Sort the source paths and place them first in the argument array
893         final ArrayList<String> toReturn = new ArrayList<String>(className2SourcePath.values());
894         Collections.sort(toReturn);
895 
896         // All Done.
897         return toReturn;
898     }
899 
900     private void printSchemaGenCommandAndThrowException(final String projectBasedirPath,
901                                                         final List<URL> sources,
902                                                         final String[] schemaGenArguments,
903                                                         final int result,
904                                                         final Throwable cause) throws MojoExecutionException {
905 
906         final StringBuilder errorMsgBuilder = new StringBuilder();
907         errorMsgBuilder.append("\n+=================== [SchemaGenerator Error '"
908                 + (result == -1 ? "<unknown>" : result) + "']\n");
909         errorMsgBuilder.append("|\n");
910         errorMsgBuilder.append("| SchemaGen did not complete its operation correctly.\n");
911         errorMsgBuilder.append("|\n");
912         errorMsgBuilder.append("| To re-create the error (and get a proper error message), cd to:\n");
913         errorMsgBuilder.append("| ").append(projectBasedirPath).append("\n");
914         errorMsgBuilder.append("| ... and fire the following on a command line/in a shell:\n");
915         errorMsgBuilder.append("|\n");
916 
917         final StringBuilder builder = new StringBuilder("schemagen ");
918         for (String current : schemaGenArguments) {
919             builder.append(current).append(" ");
920         }
921 
922         errorMsgBuilder.append("| " + builder.toString() + "\n");
923         errorMsgBuilder.append("|\n");
924         errorMsgBuilder.append("| The following source files should be processed by schemagen:\n");
925 
926         for (int i = 0; i < sources.size(); i++) {
927             errorMsgBuilder.append("| " + i + ": ").append(sources.get(i).toString()).append("\n");
928         }
929 
930         errorMsgBuilder.append("|\n");
931         errorMsgBuilder.append("+=================== [End SchemaGenerator Error]\n");
932 
933         final String msg = errorMsgBuilder.toString().replaceAll("[\r\n]+", "\n");
934         if (cause != null) {
935             throw new MojoExecutionException(msg, cause);
936         } else {
937             throw new MojoExecutionException(msg);
938         }
939     }
940 }