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