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