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