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