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