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ö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 * <configuration>
166 * <locale>fr</locale>
167 * </configuration>
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 * <configuration>
189 * ...
190 * <extraFacets>
191 * <extraFacet implementation="org.acme.MyCoolEnvironmentFacetImplementation" />
192 * </extraFacets>
193 * ...
194 * </configuration>
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 warnIfConfiguredEncodingDiffersFromFileEncoding Defines if the configured encoding is not equal to the
418 * system property {@code file.encoding}, emit a warning
419 * on the Maven Log (implies that the Maven log has to be
420 * warnEnabled).
421 * @return The encoding to be used by this AbstractJaxbMojo and its tools.
422 * @see #encoding
423 */
424 protected final String getEncoding(final boolean warnIfConfiguredEncodingDiffersFromFileEncoding) {
425
426 // Harvest information
427 final boolean configuredEncoding = encoding != null;
428 final String fileEncoding = System.getProperty(SYSTEM_FILE_ENCODING_PROPERTY);
429 final String effectiveEncoding = configuredEncoding ? encoding : fileEncoding;
430
431 // Should we warn?
432 if (warnIfConfiguredEncodingDiffersFromFileEncoding
433 && !fileEncoding.equalsIgnoreCase(effectiveEncoding)
434 && getLog().isWarnEnabled()) {
435 getLog().warn("Configured encoding [" + effectiveEncoding
436 + "] differs from encoding given in system property '" + SYSTEM_FILE_ENCODING_PROPERTY
437 + "' [" + fileEncoding + "]");
438 }
439
440 if (getLog().isDebugEnabled()) {
441 getLog().debug("Using " + (configuredEncoding ? "explicitly configured" : "system property")
442 + " encoding [" + effectiveEncoding + "]");
443 }
444
445 // All Done.
446 return effectiveEncoding;
447 }
448
449 /**
450 * Retrieves the JAXB episode File, and ensures that the parent directory where it exists is created.
451 *
452 * @param customEpisodeFileName {@code null} to indicate that the standard episode file name ("sun-jaxb.episode")
453 * should be used, and otherwise a non-empty name which should be used
454 * as the episode file name.
455 * @return A non-null File where the JAXB episode file should be written.
456 * @throws MojoExecutionException if the parent directory of the episode file could not be created.
457 */
458 protected File getEpisodeFile(final String customEpisodeFileName) throws MojoExecutionException {
459
460 // Check sanity
461 final String effectiveEpisodeFileName = customEpisodeFileName == null
462 ? "sun-jaxb.episode"
463 : customEpisodeFileName;
464 Validate.notEmpty(effectiveEpisodeFileName, "effectiveEpisodeFileName");
465
466 // Use the standard episode location
467 final File generatedMetaInfDirectory = new File(getOutputDirectory(), "META-INF");
468
469 if (!generatedMetaInfDirectory.exists()) {
470
471 FileSystemUtilities.createDirectory(generatedMetaInfDirectory, false);
472 if (getLog().isDebugEnabled()) {
473 getLog().debug("Created episode directory ["
474 + FileSystemUtilities.getCanonicalPath(generatedMetaInfDirectory) + "]: "
475 + generatedMetaInfDirectory.exists());
476 }
477 }
478
479 // All done.
480 return new File(generatedMetaInfDirectory, effectiveEpisodeFileName);
481 }
482
483 //
484 // Private helpers
485 //
486
487 private void logPluginAndJaxbDependencyInfo() {
488
489 if (getLog().isDebugEnabled()) {
490 final StringBuilder builder = new StringBuilder();
491 builder.append("\n+=================== [Brief Plugin Build Dependency Information]\n");
492 builder.append("|\n");
493 builder.append("| Note: These dependencies pertain to what was used to build *the plugin*.\n");
494 builder.append("| Check project dependencies to see the ones used in *your build*.\n");
495 builder.append("|\n");
496
497 // Find the dependency and version information within the dependencies.properties file.
498 final SortedMap<String, String> versionMap = DependsFileParser.getVersionMap(OWN_ARTIFACT_ID);
499
500 builder.append("|\n");
501 builder.append("| Plugin's own information\n");
502 builder.append("| GroupId : " + versionMap.get(DependsFileParser.OWN_GROUPID_KEY) + "\n");
503 builder.append("| ArtifactID : " + versionMap.get(DependsFileParser.OWN_ARTIFACTID_KEY) + "\n");
504 builder.append("| Version : " + versionMap.get(DependsFileParser.OWN_VERSION_KEY) + "\n");
505 builder.append("| Buildtime : " + versionMap.get(DependsFileParser.BUILDTIME_KEY) + "\n");
506 builder.append("|\n");
507 builder.append("| Plugin's JAXB-related dependencies\n");
508 builder.append("|\n");
509
510 final SortedMap<String, DependencyInfo> diMap = DependsFileParser.createDependencyInfoMap(versionMap);
511
512 int dependencyIndex = 0;
513 for (Map.Entry<String, DependencyInfo> current : diMap.entrySet()) {
514
515 final String key = current.getKey().trim();
516 for (String currentRelevantGroupId : RELEVANT_GROUPIDS) {
517 if (key.startsWith(currentRelevantGroupId)) {
518
519 final DependencyInfo di = current.getValue();
520 builder.append("| " + (++dependencyIndex) + ") [" + di.getArtifactId() + "]\n");
521 builder.append("| GroupId : " + di.getGroupId() + "\n");
522 builder.append("| ArtifactID : " + di.getArtifactId() + "\n");
523 builder.append("| Version : " + di.getVersion() + "\n");
524 builder.append("| Scope : " + di.getScope() + "\n");
525 builder.append("| Type : " + di.getType() + "\n");
526 builder.append("|\n");
527 }
528 }
529 }
530
531 builder.append("+=================== [End Brief Plugin Build Dependency Information]\n\n");
532 getLog().debug(builder.toString().replace("\n", NEWLINE));
533 }
534 }
535
536 private <T> T getInjectedObject(final T objectOrNull, final String objectName) {
537
538 if (objectOrNull == null) {
539 getLog().error(
540 "Found null '" + objectName + "', implying that Maven @Component injection was not done properly.");
541 }
542
543 return objectOrNull;
544 }
545
546 private void updateStaleFileTimestamp() throws MojoExecutionException {
547
548 final File staleFile = getStaleFile();
549 if (!staleFile.exists()) {
550
551 // Ensure that the staleFileDirectory exists
552 FileSystemUtilities.createDirectory(staleFile.getParentFile(), false);
553
554 try {
555 staleFile.createNewFile();
556
557 if (getLog().isDebugEnabled()) {
558 getLog().debug("Created staleFile [" + FileSystemUtilities.getCanonicalPath(staleFile) + "]");
559 }
560 } catch (IOException e) {
561 throw new MojoExecutionException("Could not create staleFile.", e);
562 }
563
564 } else {
565 if (!staleFile.setLastModified(System.currentTimeMillis())) {
566 getLog().warn("Failed updating modification time of staleFile ["
567 + FileSystemUtilities.getCanonicalPath(staleFile) + "]");
568 }
569 }
570 }
571
572 /**
573 * Prints out the system properties to the Maven Log at Debug level.
574 */
575 protected void logSystemPropertiesAndBasedir() {
576 if (getLog().isDebugEnabled()) {
577
578 final StringBuilder builder = new StringBuilder();
579
580 builder.append("\n+=================== [System properties]\n");
581 builder.append("|\n");
582
583 // Sort the system properties
584 final SortedMap<String, Object> props = new TreeMap<String, Object>();
585 props.put("basedir", FileSystemUtilities.getCanonicalPath(getProject().getBasedir()));
586
587 for (Map.Entry<Object, Object> current : System.getProperties().entrySet()) {
588 props.put("" + current.getKey(), current.getValue());
589 }
590 for (Map.Entry<String, Object> current : props.entrySet()) {
591 builder.append("| [" + current.getKey() + "]: " + current.getValue() + "\n");
592 }
593
594 builder.append("|\n");
595 builder.append("+=================== [End System properties]\n");
596
597 // All done.
598 getLog().debug(builder.toString().replace("\n", NEWLINE));
599 }
600 }
601 }