View Javadoc
1   package org.codehaus.mojo.jaxb2;
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 org.apache.maven.plugin.AbstractMojo;
23  import org.apache.maven.plugin.MojoExecution;
24  import org.apache.maven.plugin.MojoExecutionException;
25  import org.apache.maven.plugin.MojoFailureException;
26  import org.apache.maven.plugin.logging.Log;
27  import org.apache.maven.plugins.annotations.Component;
28  import org.apache.maven.plugins.annotations.Parameter;
29  import org.apache.maven.project.MavenProject;
30  import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
31  import org.codehaus.mojo.jaxb2.shared.Validate;
32  import org.codehaus.mojo.jaxb2.shared.environment.EnvironmentFacet;
33  import org.codehaus.mojo.jaxb2.shared.filters.Filter;
34  import org.codehaus.mojo.jaxb2.shared.filters.pattern.PatternFileFilter;
35  import org.codehaus.mojo.jaxb2.shared.version.DependencyInfo;
36  import org.codehaus.mojo.jaxb2.shared.version.DependsFileParser;
37  import org.sonatype.plexus.build.incremental.BuildContext;
38  
39  import java.io.File;
40  import java.io.IOException;
41  import java.net.URL;
42  import java.util.ArrayList;
43  import java.util.Arrays;
44  import java.util.Collections;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.SortedMap;
49  import java.util.TreeMap;
50  import java.util.regex.Pattern;
51  
52  /**
53   * Abstract Mojo which collects common infrastructure, required and needed
54   * by all subclass Mojos in the JAXB2 maven plugin codebase.
55   *
56   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>
57   */
58  public abstract class AbstractJaxbMojo extends AbstractMojo {
59  
60      /**
61       * Standard name of the generated JAXB episode file.
62       */
63      public static final String STANDARD_EPISODE_FILENAME = "sun-jaxb.episode";
64  
65      /**
66       * Standard name of the package-info.java file which may contain
67       * JAXB annotations and Package JavaDoc.
68       */
69      public static final String PACKAGE_INFO_FILENAME = "package-info.java";
70  
71      /**
72       * Platform-independent newline control string.
73       */
74      public static final String NEWLINE = System.getProperty("line.separator");
75  
76      /**
77       * Pattern matching strings containing whitespace (or consisting only of whitespace).
78       */
79      public static final Pattern CONTAINS_WHITESPACE = Pattern.compile("(\\S*\\s+\\S*)+", Pattern.UNICODE_CASE);
80  
81      /**
82       * Standard excludes Filters for all Java generator Mojos.
83       * The List is unmodifiable.
84       */
85      public static final List<Filter<File>> STANDARD_EXCLUDE_FILTERS;
86  
87      private static final List<String> RELEVANT_GROUPIDS =
88              Arrays.asList("org.glassfish.jaxb", "javax.xml.bind");
89      private static final String OWN_ARTIFACT_ID = "jaxb2-maven-plugin";
90      private static final String SYSTEM_FILE_ENCODING_PROPERTY = "file.encoding";
91      private static final String[] STANDARD_EXCLUDE_SUFFIXES = {"README.*", "\\.xml", "\\.txt"};
92  
93      static {
94  
95          // The standard exclude filters contain simple, exclude pattern filters.
96          final List<Filter<File>> tmp = new ArrayList<Filter<File>>();
97          tmp.add(new PatternFileFilter(Arrays.asList(STANDARD_EXCLUDE_SUFFIXES), true));
98  
99          // Make STANDARD_EXCLUDE_FILTERS be unmodifiable.
100         STANDARD_EXCLUDE_FILTERS = Collections.unmodifiableList(tmp);
101     }
102 
103     /**
104      * The Plexus BuildContext is used to identify files or directories modified since last build,
105      * implying functionality used to define if java generation must be performed again.
106      */
107     @Component
108     private BuildContext buildContext;
109 
110     /**
111      * The injected Maven project.
112      */
113     @Parameter(defaultValue = "${project}", readonly = true)
114     private MavenProject project;
115 
116     /**
117      * Note that the execution parameter will be injected ONLY if this plugin is executed as part
118      * of a maven standard lifecycle - as opposed to directly invoked with a direct invocation.
119      * When firing this mojo directly (i.e. {@code mvn xjc:something} or {@code mvn schemagen:something}), the
120      * {@code execution} object will not be injected.
121      */
122     @Parameter(defaultValue = "${mojoExecution}", readonly = true)
123     private MojoExecution execution;
124 
125     /**
126      * <p>The directory where the staleFile is found.
127      * The staleFile assists in determining if re-generation of JAXB build products is required.</p>
128      * <p>While it is permitted to re-define the staleFileDirectory, it is recommended to keep it
129      * below the <code>${project.build.directory}</code>, to ensure that JAXB code or XSD re-generation
130      * occurs after cleaning the project.</p>
131      *
132      * @since 2.0
133      */
134     @Parameter(defaultValue = "${project.build.directory}/jaxb2", readonly = true, required = true)
135     protected File staleFileDirectory;
136 
137     /**
138      * <p>Defines the encoding used by XJC (for generating Java Source files) and schemagen (for generating XSDs).
139      * The corresponding argument parameter for XJC and SchemaGen is: {@code encoding}.</p>
140      * <p>The algorithm for finding the encoding to use is as follows
141      * (where the first non-null value found is used for encoding):
142      * <ol>
143      * <li>If the configuration property is explicitly given within the plugin's configuration, use that value.</li>
144      * <li>If the Maven property <code>project.build.sourceEncoding</code> is defined, use its value.</li>
145      * <li>Otherwise use the value from the system property <code>file.encoding</code>.</li>
146      * </ol>
147      * </p>
148      *
149      * @see #getEncoding(boolean)
150      * @since 2.0
151      */
152     @Parameter(defaultValue = "${project.build.sourceEncoding}")
153     private String encoding;
154 
155     /**
156      * <p>A Locale definition to create and set the system (default) Locale when the XJB or SchemaGen tools executes.
157      * The Locale will be reset to its default value after the execution of XJC or SchemaGen is complete.</p>
158      * <p>The configuration parameter must be supplied on the form {@code language[,country[,variant]]},
159      * such as {@code sv,SE} or {@code fr}. Refer to
160      * {@code org.codehaus.mojo.jaxb2.shared.environment.locale.LocaleFacet.createFor(String, Log)} for further
161      * information.</p>
162      * <p><strong>Example</strong> (assigns french locale):</p>
163      * <pre>
164      *     <code>
165      *         &lt;configuration&gt;
166      *              &lt;locale&gt;fr&lt;/locale&gt;
167      *         &lt;/configuration&gt;
168      *     </code>
169      * </pre>
170      *
171      * @see org.codehaus.mojo.jaxb2.shared.environment.locale.LocaleFacet#createFor(String, Log)
172      * @see Locale#getAvailableLocales()
173      * @since 2.2
174      */
175     @Parameter(required = false)
176     protected String locale;
177 
178     /**
179      * <p>Defines a set of extra EnvironmentFacet instances which are used to further configure the
180      * ToolExecutionEnvironment used by this plugin to fire XJC or SchemaGen.</p>
181      * <p><em>Example:</em> If you implement the EnvironmentFacet interface in the class
182      * {@code org.acme.MyCoolEnvironmentFacetImplementation}, its {@code setup()} method is called before the
183      * XJC or SchemaGen tools are executed to setup some facet of their Execution environment. Correspondingly, the
184      * {@code restore()} method in your {@code org.acme.MyCoolEnvironmentFacetImplementation} class is invoked after
185      * the XJC or SchemaGen execution terminates.</p>
186      * <pre>
187      *     <code>
188      *         &lt;configuration&gt;
189      *         ...
190      *              &lt;extraFacets&gt;
191      *                  &lt;extraFacet implementation="org.acme.MyCoolEnvironmentFacetImplementation" /&gt;
192      *              &lt;/extraFacets&gt;
193      *         ...
194      *         &lt;/configuration&gt;
195      *     </code>
196      * </pre>
197      *
198      * @see EnvironmentFacet
199      * @see org.codehaus.mojo.jaxb2.shared.environment.ToolExecutionEnvironment#add(EnvironmentFacet)
200      * @since 2.2
201      */
202     @Parameter(required = false)
203     protected List<EnvironmentFacet> extraFacets;
204 
205     /**
206      * The Plexus BuildContext is used to identify files or directories modified since last build,
207      * implying functionality used to define if java generation must be performed again.
208      *
209      * @return the active Plexus BuildContext.
210      */
211     protected final BuildContext getBuildContext() {
212         return getInjectedObject(buildContext, "buildContext");
213     }
214 
215     /**
216      * @return The active MavenProject.
217      */
218     protected final MavenProject getProject() {
219         return getInjectedObject(project, "project");
220     }
221 
222     /**
223      * @return The active MojoExecution.
224      */
225     public MojoExecution getExecution() {
226         return getInjectedObject(execution, "execution");
227     }
228 
229     /**
230      * {@inheritDoc}
231      */
232     @Override
233     public final void execute() throws MojoExecutionException, MojoFailureException {
234 
235         // 0) Get the log and its relevant level
236         final Log log = getLog();
237         final boolean isDebugEnabled = log.isDebugEnabled();
238         final boolean isInfoEnabled = log.isInfoEnabled();
239 
240         // 1) Should we skip execution?
241         if (shouldExecutionBeSkipped()) {
242 
243             if (isDebugEnabled) {
244                 log.debug("Skipping execution, as instructed.");
245             }
246             return;
247         }
248 
249         // 2) Printout relevant version information.
250         if (isDebugEnabled) {
251             logPluginAndJaxbDependencyInfo();
252         }
253 
254         // 3) Are generated files stale?
255         if (isReGenerationRequired()) {
256 
257             if (performExecution()) {
258 
259                 // As instructed by the performExecution() method, update
260                 // the timestamp of the stale File.
261                 updateStaleFileTimestamp();
262 
263                 // Hack to support M2E
264                 buildContext.refresh(getOutputDirectory());
265 
266             } else if (isInfoEnabled) {
267                 log.info("Not updating staleFile timestamp as instructed.");
268             }
269         } else if (isInfoEnabled) {
270             log.info("No changes detected in schema or binding files - skipping JAXB generation.");
271         }
272 
273         // 4) If the output directories exist, add them to the MavenProject's source directories
274         if(getOutputDirectory().exists() && getOutputDirectory().isDirectory()) {
275 
276             final String canonicalPathToOutputDirectory = FileSystemUtilities.getCanonicalPath(getOutputDirectory());
277 
278             if(log.isDebugEnabled()) {
279                 log.debug("Adding existing JAXB outputDirectory [" + canonicalPathToOutputDirectory
280                         + "] to Maven's sources.");
281             }
282 
283             // Add the output Directory.
284             getProject().addCompileSourceRoot(canonicalPathToOutputDirectory);
285         }
286     }
287 
288     /**
289      * Implement this method to check if this AbstractJaxbMojo should skip executing altogether.
290      *
291      * @return {@code true} to indicate that this AbstractJaxbMojo should bail out of its execute method.
292      */
293     protected abstract boolean shouldExecutionBeSkipped();
294 
295     /**
296      * @return {@code true} to indicate that this AbstractJaxbMojo should be run since its generated files were
297      * either stale or not present, and {@code false} otherwise.
298      */
299     protected abstract boolean isReGenerationRequired();
300 
301     /**
302      * <p>Implement this method to perform this Mojo's execution.
303      * This method will only be called if {@code !shouldExecutionBeSkipped() && isReGenerationRequired()}.</p>
304      *
305      * @return {@code true} if the timestamp of the stale file should be updated.
306      * @throws MojoExecutionException if an unexpected problem occurs.
307      *                                Throwing this exception causes a "BUILD ERROR" message to be displayed.
308      * @throws MojoFailureException   if an expected problem (such as a compilation failure) occurs.
309      *                                Throwing this exception causes a "BUILD FAILURE" message to be displayed.
310      */
311     protected abstract boolean performExecution() throws MojoExecutionException, MojoFailureException;
312 
313     /**
314      * Override this method to acquire a List holding all URLs to the sources which this
315      * AbstractJaxbMojo should use to produce its output (XSDs files for AbstractXsdGeneratorMojos and
316      * Java Source Code for AbstractJavaGeneratorMojos).
317      *
318      * @return A non-null List holding URLs to sources used by this AbstractJaxbMojo to produce its output.
319      */
320     protected abstract List<URL> getSources();
321 
322     /**
323      * Retrieves the directory where the generated files should be written to.
324      *
325      * @return the directory where the generated files should be written to.
326      */
327     protected abstract File getOutputDirectory();
328 
329     /**
330      * Retrieves the configured List of paths from which this AbstractJaxbMojo and its internal toolset
331      * (XJC or SchemaGen) should read bytecode classes.
332      *
333      * @return the configured List of paths from which this AbstractJaxbMojo and its internal toolset (XJC or
334      * SchemaGen) should read classes.
335      * @throws org.apache.maven.plugin.MojoExecutionException if the classpath could not be retrieved.
336      */
337     protected abstract List<String> getClasspath() throws MojoExecutionException;
338 
339     /**
340      * Convenience method to invoke when some plugin configuration is incorrect.
341      * Will output the problem as a warning with some degree of log formatting.
342      *
343      * @param propertyName The name of the problematic property.
344      * @param description  The problem description.
345      */
346     @SuppressWarnings("all")
347     protected void warnAboutIncorrectPluginConfiguration(final String propertyName, final String description) {
348 
349         final StringBuilder builder = new StringBuilder();
350         builder.append("\n+=================== [Incorrect Plugin Configuration Detected]\n");
351         builder.append("|\n");
352         builder.append("| Property : " + propertyName + "\n");
353         builder.append("| Problem  : " + description + "\n");
354         builder.append("|\n");
355         builder.append("+=================== [End Incorrect Plugin Configuration Detected]\n\n");
356         getLog().warn(builder.toString().replace("\n", NEWLINE));
357     }
358 
359     /**
360      * @param arguments The final arguments to be passed to a JAXB tool (XJC or SchemaGen).
361      * @param toolName  The name of the tool.
362      * @return the arguments, untouched.
363      */
364     protected final String[] logAndReturnToolArguments(final String[] arguments, final String toolName) {
365 
366         // Check sanity
367         Validate.notNull(arguments, "arguments");
368 
369         if (getLog().isDebugEnabled()) {
370 
371             final StringBuilder argBuilder = new StringBuilder();
372             argBuilder.append("\n+=================== [" + arguments.length + " " + toolName + " Arguments]\n");
373             argBuilder.append("|\n");
374             for (int i = 0; i < arguments.length; i++) {
375                 argBuilder.append("| [").append(i).append("]: ").append(arguments[i]).append("\n");
376             }
377             argBuilder.append("|\n");
378             argBuilder.append("+=================== [End " + arguments.length + " " + toolName + " Arguments]\n\n");
379             getLog().debug(argBuilder.toString().replace("\n", NEWLINE));
380         }
381 
382         // All done.
383         return arguments;
384     }
385 
386     /**
387      * Retrieves the last name part of the stale file.
388      * The full name of the stale file will be generated by pre-pending {@code "." + getExecution().getExecutionId()}
389      * before this staleFileName.
390      *
391      * @return The name of the stale file used by this AbstractJavaGeneratorMojo to detect staleness amongst its
392      * generated files.
393      */
394     protected abstract String getStaleFileName();
395 
396     /**
397      * Acquires the staleFile for this execution
398      *
399      * @return the staleFile (used to define where) for this execution
400      */
401     protected final File getStaleFile() {
402         final String staleFileName = "."
403                 + (getExecution() == null ? "nonExecutionJaxb" : getExecution().getExecutionId())
404                 + "-" + getStaleFileName();
405         return new File(staleFileDirectory, staleFileName);
406     }
407 
408     /**
409      * <p>The algorithm for finding the encoding to use is as follows (where the first non-null value found
410      * is used for encoding):</p>
411      * <ol>
412      * <li>If the configuration property is explicitly given within the plugin's configuration, use that value.</li>
413      * <li>If the Maven property <code>project.build.sourceEncoding</code> is defined, use its value.</li>
414      * <li>Otherwise use the value from the system property <code>file.encoding</code>.</li>
415      * </ol>
416      *
417      * @param warnIfPlatformEncoding Defines if a warning should be logged if encoding is not configured but
418      *                               the platform encoding (system property {@code file.encoding}) is used
419      * @return The encoding to be used by this AbstractJaxbMojo and its tools.
420      * @see #encoding
421      */
422     protected final String getEncoding(final boolean warnIfPlatformEncoding) {
423 
424         // Harvest information
425         final boolean configuredEncoding = encoding != null;
426         final String fileEncoding = System.getProperty(SYSTEM_FILE_ENCODING_PROPERTY);
427         final String effectiveEncoding = configuredEncoding ? encoding : fileEncoding;
428 
429         // Should we warn if using platform encoding (i.e. platform dependent)?
430         if (!configuredEncoding && warnIfPlatformEncoding) {
431             getLog().warn("Using platform encoding ["+ effectiveEncoding + "], i.e. build is platform dependent!");
432         } else if (getLog().isDebugEnabled()) {
433             getLog().debug("Using " + (configuredEncoding ? "explicitly configured" : "system property")
434                     + " encoding [" + effectiveEncoding + "]");
435         }
436 
437         // All Done.
438         return effectiveEncoding;
439     }
440 
441     /**
442      * Retrieves the JAXB episode File, and ensures that the parent directory where it exists is created.
443      *
444      * @param customEpisodeFileName {@code null} to indicate that the standard episode file name ("sun-jaxb.episode")
445      *                              should be used, and otherwise a non-empty name which should be used
446      *                              as the episode file name.
447      * @return A non-null File where the JAXB episode file should be written.
448      * @throws MojoExecutionException if the parent directory of the episode file could not be created.
449      */
450     protected File getEpisodeFile(final String customEpisodeFileName) throws MojoExecutionException {
451 
452         // Check sanity
453         final String effectiveEpisodeFileName = customEpisodeFileName == null
454                 ? "sun-jaxb.episode"
455                 : customEpisodeFileName;
456         Validate.notEmpty(effectiveEpisodeFileName, "effectiveEpisodeFileName");
457 
458         // Use the standard episode location
459         final File generatedMetaInfDirectory = new File(getOutputDirectory(), "META-INF");
460 
461         if (!generatedMetaInfDirectory.exists()) {
462 
463             FileSystemUtilities.createDirectory(generatedMetaInfDirectory, false);
464             if (getLog().isDebugEnabled()) {
465                 getLog().debug("Created episode directory ["
466                         + FileSystemUtilities.getCanonicalPath(generatedMetaInfDirectory) + "]: "
467                         + generatedMetaInfDirectory.exists());
468             }
469         }
470 
471         // All done.
472         return new File(generatedMetaInfDirectory, effectiveEpisodeFileName);
473     }
474 
475     //
476     // Private helpers
477     //
478 
479     private void logPluginAndJaxbDependencyInfo() {
480 
481         if (getLog().isDebugEnabled()) {
482             final StringBuilder builder = new StringBuilder();
483             builder.append("\n+=================== [Brief Plugin Build Dependency Information]\n");
484             builder.append("|\n");
485             builder.append("| Note: These dependencies pertain to what was used to build *the plugin*.\n");
486             builder.append("|       Check project dependencies to see the ones used in *your build*.\n");
487             builder.append("|\n");
488 
489             // Find the dependency and version information within the dependencies.properties file.
490             final SortedMap<String, String> versionMap = DependsFileParser.getVersionMap(OWN_ARTIFACT_ID);
491 
492             builder.append("|\n");
493             builder.append("| Plugin's own information\n");
494             builder.append("|     GroupId    : " + versionMap.get(DependsFileParser.OWN_GROUPID_KEY) + "\n");
495             builder.append("|     ArtifactID : " + versionMap.get(DependsFileParser.OWN_ARTIFACTID_KEY) + "\n");
496             builder.append("|     Version    : " + versionMap.get(DependsFileParser.OWN_VERSION_KEY) + "\n");
497             builder.append("|     Buildtime  : " + versionMap.get(DependsFileParser.BUILDTIME_KEY) + "\n");
498             builder.append("|\n");
499             builder.append("| Plugin's JAXB-related dependencies\n");
500             builder.append("|\n");
501 
502             final SortedMap<String, DependencyInfo> diMap = DependsFileParser.createDependencyInfoMap(versionMap);
503 
504             int dependencyIndex = 0;
505             for (Map.Entry<String, DependencyInfo> current : diMap.entrySet()) {
506 
507                 final String key = current.getKey().trim();
508                 for (String currentRelevantGroupId : RELEVANT_GROUPIDS) {
509                     if (key.startsWith(currentRelevantGroupId)) {
510 
511                         final DependencyInfo di = current.getValue();
512                         builder.append("|   " + (++dependencyIndex) + ") [" + di.getArtifactId() + "]\n");
513                         builder.append("|     GroupId    : " + di.getGroupId() + "\n");
514                         builder.append("|     ArtifactID : " + di.getArtifactId() + "\n");
515                         builder.append("|     Version    : " + di.getVersion() + "\n");
516                         builder.append("|     Scope      : " + di.getScope() + "\n");
517                         builder.append("|     Type       : " + di.getType() + "\n");
518                         builder.append("|\n");
519                     }
520                 }
521             }
522 
523             builder.append("+=================== [End Brief Plugin Build Dependency Information]\n\n");
524             getLog().debug(builder.toString().replace("\n", NEWLINE));
525         }
526     }
527 
528     private <T> T getInjectedObject(final T objectOrNull, final String objectName) {
529 
530         if (objectOrNull == null) {
531             getLog().error(
532                     "Found null '" + objectName + "', implying that Maven @Component injection was not done properly.");
533         }
534 
535         return objectOrNull;
536     }
537 
538     private void updateStaleFileTimestamp() throws MojoExecutionException {
539 
540         final File staleFile = getStaleFile();
541         if (!staleFile.exists()) {
542 
543             // Ensure that the staleFileDirectory exists
544             FileSystemUtilities.createDirectory(staleFile.getParentFile(), false);
545 
546             try {
547                 staleFile.createNewFile();
548 
549                 if (getLog().isDebugEnabled()) {
550                     getLog().debug("Created staleFile [" + FileSystemUtilities.getCanonicalPath(staleFile) + "]");
551                 }
552             } catch (IOException e) {
553                 throw new MojoExecutionException("Could not create staleFile.", e);
554             }
555 
556         } else {
557             if (!staleFile.setLastModified(System.currentTimeMillis())) {
558                 getLog().warn("Failed updating modification time of staleFile ["
559                         + FileSystemUtilities.getCanonicalPath(staleFile) + "]");
560             }
561         }
562     }
563 
564     /**
565      * Prints out the system properties to the Maven Log at Debug level.
566      */
567     protected void logSystemPropertiesAndBasedir() {
568         if (getLog().isDebugEnabled()) {
569 
570             final StringBuilder builder = new StringBuilder();
571 
572             builder.append("\n+=================== [System properties]\n");
573             builder.append("|\n");
574 
575             // Sort the system properties
576             final SortedMap<String, Object> props = new TreeMap<String, Object>();
577             props.put("basedir", FileSystemUtilities.getCanonicalPath(getProject().getBasedir()));
578 
579             for (Map.Entry<Object, Object> current : System.getProperties().entrySet()) {
580                 props.put("" + current.getKey(), current.getValue());
581             }
582             for (Map.Entry<String, Object> current : props.entrySet()) {
583                 builder.append("| [" + current.getKey() + "]: " + current.getValue() + "\n");
584             }
585 
586             builder.append("|\n");
587             builder.append("+=================== [End System properties]\n");
588 
589             // All done.
590             getLog().debug(builder.toString().replace("\n", NEWLINE));
591         }
592     }
593 }