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