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