1 package org.codehaus.mojo.jaxb2.javageneration;
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.xjc.Driver;
23 import org.apache.maven.model.Resource;
24 import org.apache.maven.plugin.MojoExecutionException;
25 import org.apache.maven.plugin.MojoFailureException;
26 import org.apache.maven.plugins.annotations.Parameter;
27 import org.apache.maven.settings.Proxy;
28 import org.apache.maven.settings.Settings;
29 import org.codehaus.mojo.jaxb2.AbstractJaxbMojo;
30 import org.codehaus.mojo.jaxb2.NoSchemasException;
31 import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
32 import org.codehaus.mojo.jaxb2.shared.arguments.ArgumentBuilder;
33 import org.codehaus.mojo.jaxb2.shared.environment.ToolExecutionEnvironment;
34 import org.codehaus.mojo.jaxb2.shared.environment.classloading.ThreadContextClassLoaderBuilder;
35 import org.codehaus.mojo.jaxb2.shared.environment.logging.LoggingHandlerEnvironmentFacet;
36 import org.codehaus.plexus.util.FileUtils;
37 import org.codehaus.plexus.util.IOUtil;
38
39 import java.io.File;
40 import java.io.FileWriter;
41 import java.net.HttpURLConnection;
42 import java.net.URL;
43 import java.net.URLConnection;
44 import java.util.ArrayList;
45 import java.util.List;
46
47 /**
48 * <p>Abstract superclass for Mojos generating Java source or binaries from XML schema(s) by invoking the JAXB XJC
49 * binding compiler. Most of the Configuration options for the AbstractJavaGeneratorMojo are set or copied to the
50 * XJC directly; refer to their documentation in the <a href="https://jaxb.java.net/">JAXB Reference Implementation</a>
51 * site.</p>
52 *
53 * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>
54 * @see <a href="https://jaxb.java.net/">The JAXB Reference Implementation</a>
55 */
56 public abstract class AbstractJavaGeneratorMojo extends AbstractJaxbMojo {
57
58 private static final int XJC_COMPLETED_OK = 0;
59
60 /**
61 * <p>Corresponding XJC parameter: {@code catalog}.</p>
62 * <p>Specify catalog files to resolve external entity references.
63 * Supports TR9401, XCatalog, and OASIS XML Catalog format.</p>
64 */
65 @Parameter
66 protected File catalog;
67
68 /**
69 * <p>Corresponding XJC parameter: {@code episode}.</p>
70 * <p>Generate an episode file from this compilation, so that other schemas that rely on this schema can be
71 * compiled later and rely on classes that are generated from this compilation. The generated episode file is
72 * really just a JAXB customization file (but with vendor extensions.)</p>
73 * <p>If this parameter is {@code true}, the episode file generated is called {@code META-INF/sun-jaxb.episode},
74 * and included in the artifact.</p>
75 *
76 * @see #STANDARD_EPISODE_FILENAME
77 * @since 2.0
78 */
79 @Parameter(defaultValue = "true")
80 protected boolean generateEpisode;
81
82 /**
83 * <p>Sets the HTTP/HTTPS proxy to be used by the XJC, on the format
84 * {@code [user[:password]@]proxyHost[:proxyPort]}.
85 * All information is retrieved from the active proxy within the standard maven settings file.</p>
86 */
87 @Parameter(defaultValue = "${settings}", readonly = true)
88 protected Settings settings;
89
90 /**
91 * <p>Defines the content type of sources for the XJC. To simplify usage of the JAXB2 maven plugin,
92 * all source files are assumed to have the same type of content.</p>
93 * <p>This parameter replaces the previous multiple-choice boolean configuration options for the
94 * jaxb2-maven-plugin (i.e. dtd, xmlschema, relaxng, relaxng-compact, wsdl), and
95 * corresponds to setting one of those flags as an XJC argument.</p>
96 *
97 * @since 2.0
98 */
99 @Parameter(defaultValue = "XmlSchema")
100 protected SourceContentType sourceType;
101
102 /**
103 * <p>Corresponding XJC parameter: {@code npa}.</p>
104 * <p>Suppress the generation of package level annotations into {@code package-info.java}.
105 * Using this switch causes the generated code to internalize those annotations into the other
106 * generated classes.</p>
107 *
108 * @since 2.0
109 */
110 @Parameter(defaultValue = "false")
111 protected boolean noPackageLevelAnnotations;
112
113 /**
114 * <p>Corresponding XJC parameter: {@code no-header}.</p>
115 * <p>Suppress the generation of a file header comment that includes some note and timestamp.
116 * Using this makes the generated code more diff-friendly.</p>
117 *
118 * @since 2.0
119 */
120 @Parameter(defaultValue = "false")
121 protected boolean noGeneratedHeaderComments;
122
123 /**
124 * <p>Corresponding XJC parameter: {@code mark-generated}.</p>
125 * <p>This feature causes all of the generated code to have {@code @Generated} annotation.</p>
126 *
127 * @since 2.0
128 */
129 @Parameter(defaultValue = "false")
130 protected boolean addGeneratedAnnotation;
131
132 /**
133 * <p>Corresponding XJC parameter: {@code nv}.</p>
134 * <p>By default, the XJC binding compiler performs strict validation of the source schema before processing it.
135 * Use this option to disable strict schema validation. This does not mean that the binding compiler will not
136 * perform any validation, it simply means that it will perform less-strict validation.</p>
137 *
138 * @since 2.0
139 */
140 @Parameter(defaultValue = "false")
141 protected boolean laxSchemaValidation;
142
143 /**
144 * <p>Corresponding XJC parameter: {@code quiet}.</p>
145 * <p>Suppress compiler output, such as progress information and warnings.</p>
146 */
147 @Parameter(defaultValue = "false")
148 protected boolean quiet;
149
150 /**
151 * <p>Corresponding XJC parameter: {@code verbose}.</p>
152 * <p>Tells XJC to be extra verbose, such as printing informational messages or displaying stack traces.</p>
153 */
154 @Parameter(property = "xjc.verbose", defaultValue = "false")
155 protected boolean verbose;
156
157 /**
158 * <p>Corresponding XJC parameter: {@code extension}.</p>
159 * <p>By default, the XJC binding compiler strictly enforces the rules outlined in the Compatibility chapter of
160 * the JAXB Specification. Appendix E.2 defines a set of W3C XML Schema features that are not completely
161 * supported by JAXB v1.0. In some cases, you may be allowed to use them in the "-extension" mode enabled by
162 * this switch. In the default (strict) mode, you are also limited to using only the binding customizations
163 * defined in the specification.</p>
164 */
165 @Parameter(defaultValue = "false")
166 protected boolean extension;
167
168 /**
169 * Fails the Mojo execution if no XSDs/schemas are found.
170 *
171 * @since 1.3
172 */
173 @Parameter(defaultValue = "true")
174 protected boolean failOnNoSchemas;
175
176 /**
177 * <p>Removes all files from the output directory before running XJC.</p>
178 */
179 @Parameter(defaultValue = "true")
180 protected boolean clearOutputDir;
181
182 /**
183 * <p>Corresponding XJC parameter: {@code readOnly}.</p>
184 * <p>By default, the XJC binding compiler does not write-protect the Java source files it generates.
185 * Use this option to force the XJC binding compiler to mark the generated Java sources read-only.</p>
186 *
187 * @since 2.0
188 */
189 @Parameter(defaultValue = "false")
190 protected boolean readOnly;
191
192 /**
193 * <p>List of ordered extra arguments to the XJC command. Each extra argument is interpreted as a word, intended
194 * to be copied verbatim to the XJC argument list with spaces in between:</p>
195 * <pre>
196 * <code>
197 * <configuration>
198 * ...
199 * <arguments>
200 * <argument>-Xfluent-api</argument>
201 * <argument>somefile</argument>
202 * </arguments>
203 * </configuration>
204 * </code>
205 * </pre>
206 * <p>The arguments configured above yields the following extra arguments to the XJC command:
207 * <code>-Xfluent-api -episode somefile</code></p>
208 *
209 * @since 2.0
210 * @deprecated This should be removed in the 2.0+ release, as all arguments should be handled by other parameters.
211 */
212 @Parameter(property = "xjc.arguments")
213 protected List<String> arguments;
214
215 /**
216 * <p>Corresponding XJC parameter: {@code enableIntrospection}.</p>
217 * <p>Enable correct generation of Boolean getters/setters to enable Bean Introspection APIs.</p>
218 *
219 * @since 1.4
220 */
221 @Parameter(defaultValue = "false")
222 private boolean enableIntrospection;
223
224 /**
225 * <p>Corresponding XJC parameter: {@code p}.</p>
226 * <p>The package under which the source files will be generated. Quoting the XJC documentation:
227 * "Specifying a target package via this command-line option overrides any binding customization for package
228 * name and the default package name algorithm defined in the specification".</p>
229 */
230 @Parameter
231 protected String packageName;
232
233 /**
234 * <p>Corresponding XJC parameter: {@code target}.</p>
235 * <p>Permitted values: {@code "2.0"} and {@code "2.1"}. Avoid generating code that relies on JAXB newer than the
236 * version given. This will allow the generated code to run with JAXB 2.0 runtime (such as JavaSE 6.)</p>
237 *
238 * @since 1.3
239 */
240 @Parameter
241 protected String target;
242
243 /**
244 * <p>If provided, this parameter indicates that the XSDs used by XJC to generate Java code should be
245 * copied into the resulting artifact of this project (the JAR, WAR or whichever artifact type is generated).
246 * The value of the {@code xsdPathWithinArtifact} parameter is the relative path within the artifact where
247 * all source XSDs are copied to (hence the name "XSD Path Within Artifact").</p>
248 * <p>The target directory is created within the artifact if it does not already exist.
249 * If the {@code xsdPathWithinArtifact} parameter is not given, the XSDs used to generate Java code are
250 * <em>not</em> included within the project's artifact.</p>
251 * <p><em>Example:</em>Adding the sample configuration below would copy all source XSDs to the given directory
252 * within the resulting JAR (and/or test-JAR). If the directory {@code META-INF/jaxb/xsd} does not exist, it
253 * will be created.</p>
254 * <pre>
255 * <code>
256 * <configuration>
257 * ...
258 * <xsdPathWithinArtifact>META-INF/jaxb/xsd</xsdPathWithinArtifact>
259 * </configuration>
260 * </code>
261 * </pre>
262 * <p><strong>Note</strong>: This parameter was previously called {@code includeSchemasOutputPath}
263 * in the 1.x versions of this plugin, but was renamed and re-documented for improved usability and clarity.</p>
264 *
265 * @since 2.0
266 */
267 @Parameter
268 protected String xsdPathWithinArtifact;
269
270 /**
271 * <p>Java generation is required if any of the file products is outdated/stale.</p>
272 * {@inheritDoc}
273 */
274 @Override
275 protected boolean isReGenerationRequired() {
276
277 //
278 // Use the stale flag method to identify if we should re-generate the java source code from the supplied
279 // Xml Schema. Basically, we should regenerate the JAXB code if:
280 //
281 // a) The staleFile does not exist
282 // b) The staleFile exists and is older than one of the sources (XSD or XJB files).
283 // "Older" is determined by comparing the modification timestamp of the staleFile and the source files.
284 //
285 final File staleFile = getStaleFile();
286 final String debugPrefix = "StaleFile [" + FileSystemUtilities.getCanonicalPath(staleFile) + "]";
287
288 boolean stale = !staleFile.exists();
289 if (stale) {
290 getLog().debug(debugPrefix + " not found. JAXB (re-)generation required.");
291 } else {
292
293 final List<URL> sourceXSDs = getSources();
294 final List<File> sourceXJBs = getSourceXJBs();
295
296 if (getLog().isDebugEnabled()) {
297 getLog().debug(debugPrefix + " found. Checking timestamps on source XSD and XJB "
298 + "files to determine if JAXB (re-)generation is required.");
299 }
300
301 final long staleFileLastModified = staleFile.lastModified();
302 for (URL current : sourceXSDs) {
303
304 final URLConnection sourceXsdConnection;
305 try {
306 sourceXsdConnection = current.openConnection();
307 sourceXsdConnection.connect();
308 } catch (Exception e) {
309
310 // Can't determine if the staleFile is younger than this sourceXSD.
311 // Re-generate to be on the safe side.
312 stale = true;
313 break;
314 }
315
316 try {
317 if (sourceXsdConnection.getLastModified() > staleFileLastModified) {
318
319 if (getLog().isDebugEnabled()) {
320 getLog().debug(current.toString() + " is newer than the stale flag file.");
321 }
322 stale = true;
323 }
324 } finally {
325 if (sourceXsdConnection instanceof HttpURLConnection) {
326 ((HttpURLConnection) sourceXsdConnection).disconnect();
327 }
328 }
329 }
330
331 for (File current : sourceXJBs) {
332 if (current.lastModified() > staleFileLastModified) {
333
334 if (getLog().isDebugEnabled()) {
335 getLog().debug(FileSystemUtilities.getCanonicalPath(current)
336 + " is newer than the stale flag file.");
337 }
338
339 stale = true;
340 break;
341 }
342 }
343 }
344
345 // All done.
346 return stale;
347 }
348
349 /**
350 * {@inheritDoc}
351 */
352 @Override
353 protected boolean performExecution() throws MojoExecutionException, MojoFailureException {
354
355 boolean updateStaleFileTimestamp = false;
356
357 try {
358
359 // Setup the Tool's execution environment
360 ToolExecutionEnvironment environment = null;
361 try {
362
363 // Create the ToolExecutionEnvironment
364 environment = new ToolExecutionEnvironment(getLog(),
365 ThreadContextClassLoaderBuilder.createFor(this.getClass(), getLog()).addPaths(getClasspath()),
366 LoggingHandlerEnvironmentFacet.create(getLog(), getClass(), getEncoding(false)));
367 environment.setup();
368
369 // Compile the XJC arguments
370 final String[] xjcArguments = getXjcArguments(
371 environment.getClassPathAsArgument(),
372 STANDARD_EPISODE_FILENAME);
373
374 // Ensure that the outputDirectory exists, but only clear it if does not already
375 FileSystemUtilities.createDirectory(getOutputDirectory(), clearOutputDir);
376
377 // Do we need to re-create the episode file's parent directory.
378 final boolean reCreateEpisodeFileParentDirectory = generateEpisode && clearOutputDir;
379 if (reCreateEpisodeFileParentDirectory) {
380 getEpisodeFile(STANDARD_EPISODE_FILENAME);
381 }
382
383 // Fire XJC
384 if (XJC_COMPLETED_OK != Driver.run(xjcArguments, new XjcLogAdapter(getLog()))) {
385
386 final StringBuilder errorMsgBuilder = new StringBuilder();
387 errorMsgBuilder.append("\n+=================== [XJC Error]\n");
388 errorMsgBuilder.append("|\n");
389
390 final List<URL> sourceXSDs = getSources();
391 for (int i = 0; i < sourceXSDs.size(); i++) {
392 errorMsgBuilder.append("| " + i + ": ").append(sourceXSDs.get(i).toString()).append("\n");
393 }
394
395 errorMsgBuilder.append("|\n");
396 errorMsgBuilder.append("+=================== [End XJC Error]\n");
397 throw new MojoExecutionException(errorMsgBuilder.toString());
398 }
399
400 // Indicate that the output directory was updated.
401 getBuildContext().refresh(getOutputDirectory());
402
403 // Update the modification timestamp of the staleFile.
404 updateStaleFileTimestamp = true;
405
406 } finally {
407
408 if (environment != null) {
409 environment.restore();
410 }
411 }
412
413 // Add the generated source root to the project, enabling tooling and other plugins to see them.
414 addGeneratedSourcesToProjectSourceRoot();
415
416 // Copy all source XSDs to the resulting artifact?
417 if (xsdPathWithinArtifact != null) {
418
419 final String buildOutputDirectory = getProject().getBuild().getOutputDirectory();
420 final File targetXsdDirectory = new File(buildOutputDirectory, xsdPathWithinArtifact);
421 FileUtils.forceMkdir(targetXsdDirectory);
422
423 for (URL current : getSources()) {
424
425 String fileName = null;
426 if ("file".equalsIgnoreCase(current.getProtocol())) {
427 fileName = new File(current.getPath()).getName();
428 } else if ("jar".equalsIgnoreCase(current.getProtocol())) {
429
430 // Typical JAR path
431 // jar:file:/path/to/aJar.jar!/some/path/xsd/aResource.xsd
432 final int bangIndex = current.toString().indexOf("!");
433 if (bangIndex == -1) {
434 throw new MojoExecutionException("Illegal JAR URL [" + current.toString()
435 + "]: lacks a '!'");
436 }
437
438 final String internalPath = current.toString().substring(bangIndex + 1);
439 fileName = new File(internalPath).getName();
440 } else {
441 throw new MojoExecutionException("Could not extract FileName from URL [" + current + "]");
442 }
443
444 final File targetFile = new File(targetXsdDirectory, fileName);
445 if (targetFile.exists()) {
446
447 // TODO: Should we throw an exception here instead?
448 getLog().warn("File [" + FileSystemUtilities.getCanonicalPath(targetFile)
449 + "] already exists. Not copying XSD file [" + current.getPath() + "] to it.");
450 }
451 IOUtil.copy(current.openStream(), new FileWriter(targetFile));
452 }
453
454 // Refresh the BuildContext
455 getBuildContext().refresh(targetXsdDirectory);
456 }
457 } catch (MojoExecutionException e) {
458 throw e;
459 } catch (NoSchemasException e) {
460 if (failOnNoSchemas) {
461 throw new MojoExecutionException("", e);
462 }
463 } catch (Exception e) {
464 throw new MojoExecutionException(e.getMessage(), e);
465 }
466
467 // All done.
468 return updateStaleFileTimestamp;
469 }
470
471 /**
472 * Override this method to acquire a List holding all URLs to the JAXB sources for which this
473 * AbstractJavaGeneratorMojo should generate Java files. Sources are assumed to be in the form given by
474 * the {@code sourceType} value.
475 *
476 * @return A non-null List holding URLs to sources for the XJC generation.
477 * @see #sourceType
478 */
479 @Override
480 protected abstract List<URL> getSources();
481
482 /**
483 * Override this method to retrieve a list of Files to all XJB files for which this
484 * AbstractJavaGeneratorMojo should generate Java files.
485 *
486 * @return A non-null List holding binding files.
487 */
488 protected abstract List<File> getSourceXJBs();
489
490 /**
491 * Adds any directories containing the generated XJC classes to the appropriate Project compilation sources;
492 * either {@code TestCompileSourceRoot} or {@code CompileSourceRoot} depending on the exact Mojo implementation
493 * of this AbstractJavaGeneratorMojo.
494 */
495 protected abstract void addGeneratedSourcesToProjectSourceRoot();
496
497 /**
498 * Adds the supplied Resource to the project using the appropriate scope (i.e. resource or testResource)
499 * depending on the exact implementation of this AbstractJavaGeneratorMojo.
500 *
501 * @param resource The resource to add.
502 */
503 protected abstract void addResource(final Resource resource);
504
505 //
506 // Private helpers
507 //
508
509 private void addResourceDirectory(final File directoryOrNull) {
510
511 if (directoryOrNull != null) {
512
513 // Wrap the given directory in a Resource
514 final Resource toAdd = new Resource();
515 toAdd.setDirectory(directoryOrNull.getAbsolutePath());
516
517 // Add the resource to the appropriate location.
518 addResource(toAdd);
519
520 // Refresh the build context
521 getBuildContext().refresh(directoryOrNull);
522 }
523 }
524
525 private String[] getXjcArguments(final String classPath, final String episodeFileNameOrNull)
526 throws MojoExecutionException, NoSchemasException {
527
528 final ArgumentBuilder builder = new ArgumentBuilder();
529
530 // Add all flags on the form '-flagName'
531 builder.withFlag(true, sourceType.getXjcArgument());
532 builder.withFlag(noPackageLevelAnnotations, "npa");
533 builder.withFlag(laxSchemaValidation, "nv");
534 builder.withFlag(verbose, "verbose");
535 builder.withFlag(quiet, "quiet");
536 builder.withFlag(enableIntrospection, "enableIntrospection");
537 builder.withFlag(extension, "extension");
538 builder.withFlag(readOnly, "readOnly");
539 builder.withFlag(noGeneratedHeaderComments, "no-header");
540 builder.withFlag(addGeneratedAnnotation, "mark-generated");
541
542 // Add all arguments on the form '-argumentName argumentValue'
543 // (i.e. in 2 separate elements of the returned String[])
544 builder.withNamedArgument("httpproxy", getProxyString(settings.getActiveProxy()));
545 builder.withNamedArgument("encoding", getEncoding(true));
546 builder.withNamedArgument("p", packageName);
547 builder.withNamedArgument("target", target);
548 builder.withNamedArgument("d", getOutputDirectory().getAbsolutePath());
549 builder.withNamedArgument("classpath", classPath);
550
551 if (generateEpisode) {
552
553 // We must use the -extension flag for the episode to work.
554 if (!extension) {
555
556 if (getLog().isInfoEnabled()) {
557 getLog().info("Adding 'extension' flag to XJC arguments, since the 'generateEpisode' argument is "
558 + "given. (XJCs 'episode' argument requires that the 'extension' argument is provided).");
559 }
560 builder.withFlag(true, "extension");
561 }
562
563 final File episodeFile = getEpisodeFile(episodeFileNameOrNull);
564 builder.withNamedArgument("episode", FileSystemUtilities.getCanonicalPath(episodeFile));
565 }
566 if (catalog != null) {
567 builder.withNamedArgument("catalog", FileSystemUtilities.getCanonicalPath(catalog));
568 }
569
570 if (arguments != null) {
571 builder.withPreCompiledArguments(arguments);
572 }
573
574 for (File current : getSourceXJBs()) {
575
576 // Shorten the argument if possible.
577 final String strippedXjbPath = FileSystemUtilities.relativize(
578 current.getAbsolutePath(), getProject().getBasedir());
579
580 // Each XJB must be given as a separate argument.
581 builder.withNamedArgument("-b", strippedXjbPath);
582 }
583
584 final List<URL> sourceXSDs = getSources();
585 if (sourceXSDs.isEmpty()) {
586
587 // If we have no XSDs, we are not going to be able to run XJC.
588 getLog().warn("No XSD files found. Please check your plugin configuration.");
589 throw new NoSchemasException();
590
591 } else {
592
593 final List<String> unwrappedSourceXSDs = new ArrayList<String>();
594 for (URL current : sourceXSDs) {
595
596 // Shorten the argument if possible.
597 if ("file".equalsIgnoreCase(current.getProtocol())) {
598 unwrappedSourceXSDs.add(FileSystemUtilities.relativize(
599 current.getPath(),
600 getProject().getBasedir()));
601 } else {
602 unwrappedSourceXSDs.add(current.toString());
603 }
604 }
605
606 builder.withPreCompiledArguments(unwrappedSourceXSDs);
607 }
608
609 // All done.
610 return logAndReturnToolArguments(builder.build(), "XJC");
611 }
612
613 private String getProxyString(final Proxy activeProxy) {
614
615 // Check sanity
616 if (activeProxy == null) {
617 return null;
618 }
619
620 // The XJC proxy argument should be on the form
621 // [user[:password]@]proxyHost[:proxyPort]
622 //
623 // builder.withNamedArgument("httpproxy", httpproxy);
624 //
625 final StringBuilder proxyBuilder = new StringBuilder();
626 if (activeProxy.getUsername() != null) {
627
628 // Start with the username.
629 proxyBuilder.append(activeProxy.getUsername());
630
631 // Append the password if provided.
632 if (activeProxy.getPassword() != null) {
633 proxyBuilder.append(":").append(activeProxy.getPassword());
634 }
635
636 proxyBuilder.append("@");
637 }
638
639 // Append hostname and port.
640 proxyBuilder.append(activeProxy.getHost()).append(":").append(activeProxy.getPort());
641
642 // All done.
643 return proxyBuilder.toString();
644 }
645 }