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