1 package org.codehaus.mojo.jaxb2.schemageneration;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
69
70
71
72
73
74
75
76 public abstract class AbstractXsdGeneratorMojo extends AbstractJaxbMojo {
77
78
79
80
81
82
83 public static final Pattern SCHEMAGEN_EMITTED_FILENAME = Pattern.compile("schema\\p{javaDigit}+.xsd");
84
85
86
87
88
89
90
91 public static final JavaDocRenderer STANDARD_JAVADOC_RENDERER = new DefaultJavaDocRenderer();
92
93
94
95
96
97 public static final List<Filter<File>> STANDARD_BYTECODE_EXCLUDE_FILTERS;
98
99
100
101
102 public static final List<Filter<File>> CLASS_INCLUDE_FILTERS;
103
104
105
106
107
108
109
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
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164 @Parameter
165 private List<TransformSchema> transformSchemas;
166
167
168
169
170
171
172
173
174
175
176
177
178 @Parameter(defaultValue = "true")
179 protected boolean generateEpisode;
180
181
182
183
184
185
186
187
188 @Parameter(defaultValue = "true")
189 protected boolean createJavaDocAnnotations;
190
191
192
193
194
195
196
197
198
199 @Parameter
200 protected JavaDocRenderer javaDocRenderer;
201
202
203
204
205
206
207 @Parameter(defaultValue = "true")
208 protected boolean clearOutputDir;
209
210
211
212
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
230 return toReturn;
231 }
232
233
234
235
236 @Override
237 protected boolean isReGenerationRequired() {
238
239
240
241
242
243
244
245
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
276
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
298 return stale;
299 }
300
301
302
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
314
315
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
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
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
339 if (extraFacets != null) {
340 for (EnvironmentFacet current : extraFacets) {
341 environment.add(current);
342 }
343 }
344
345
346 environment.setup();
347
348
349 final List<URL> sources = getSources();
350 final String[] schemaGenArguments = getSchemaGenArguments(
351 environment.getClassPathAsArgument(),
352 STANDARD_EPISODE_FILENAME,
353 sources);
354
355
356
357 FileSystemUtilities.createDirectory(getOutputDirectory(), clearOutputDir);
358 FileSystemUtilities.createDirectory(getWorkDirectory(), clearOutputDir);
359
360
361 final boolean reCreateEpisodeFileParentDirectory = generateEpisode && clearOutputDir;
362 if (reCreateEpisodeFileParentDirectory) {
363 getEpisodeFile(STANDARD_EPISODE_FILENAME);
364 }
365
366 try {
367
368
369
370
371
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
385 throw new MojoExecutionException("JAXB errors arose while SchemaGen compiled sources to XML.");
386 }
387
388
389
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
399 final String currentPath = FileSystemUtilities.getCanonicalPath(current.getAbsoluteFile());
400 final File target = new File(getOutputDirectory(),
401 FileSystemUtilities.relativize(currentPath, getWorkDirectory()));
402
403
404 FileSystemUtilities.createDirectory(target.getParentFile(), false);
405 FileUtils.copyFile(current, target);
406 }
407
408
409
410
411
412
413
414
415
416
417 final boolean performPostProcessing = createJavaDocAnnotations || transformSchemas != null;
418 if (performPostProcessing) {
419
420
421
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
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
450 final JavaDocExtractor extractor = new JavaDocExtractor(getLog()).addSourceFiles(files);
451 final SearchableDocumentation javaDocs = extractor.process();
452
453
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
474 XsdGeneratorHelper.replaceNamespacePrefixes(resolverMap,
475 transformSchemas,
476 getLog(),
477 getOutputDirectory());
478
479
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
492
493
494 Throwable current = e;
495 while (current.getCause() != null) {
496 current = current.getCause();
497 }
498
499 getLog().error("Execution failed.");
500
501
502
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
522 getBuildContext().refresh(getOutputDirectory());
523
524
525 updateStaleFileTimestamp = true;
526
527 } finally {
528
529
530 if (environment != null) {
531 environment.restore();
532 }
533 }
534
535
536 return updateStaleFileTimestamp;
537 }
538
539
540
541
542
543 protected abstract File getWorkDirectory();
544
545
546
547
548
549
550
551
552
553
554 protected abstract List<URL> getCompiledClassNames();
555
556
557
558
559
560
561
562 @Override
563 protected abstract List<URL> getSources();
564
565
566
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
577
578
579
580
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
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
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
623 return logAndReturnToolArguments(builder.build(), "SchemaGen");
624 }
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
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
662 for (URL current : sources) {
663
664 final File sourceCodeFile = FileSystemUtilities.getFileFor(current, encoding);
665
666
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
681 final JavaProjectBuilder builder = new JavaProjectBuilder();
682 builder.setEncoding(encoding);
683
684
685
686
687 if (sourceCodeFile.getName().trim().equalsIgnoreCase(PACKAGE_INFO_FILENAME)) {
688
689
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
698 final JavaPackage javaPackage = packages.iterator().next();
699 className2SourcePath.put("package-info for (" + javaPackage.getName() + ")", relativePath);
700 continue;
701 }
702
703
704 builder.addSource(sourceCodeFile);
705
706
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
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
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
856 final ArrayList<String> toReturn = new ArrayList<String>(className2SourcePath.values());
857 Collections.sort(toReturn);
858
859
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 }