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