1 package org.apache.maven.archetype.mojos;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.commons.collections.CollectionUtils;
23 import org.apache.maven.archetype.ArchetypeGenerationRequest;
24 import org.apache.maven.archetype.ArchetypeGenerationResult;
25 import org.apache.maven.archetype.common.Constants;
26 import org.apache.maven.archetype.exception.ArchetypeNotConfigured;
27 import org.apache.maven.archetype.generator.ArchetypeGenerator;
28 import org.apache.maven.plugin.AbstractMojo;
29 import org.apache.maven.plugin.MojoExecutionException;
30 import org.apache.maven.plugin.MojoFailureException;
31 import org.apache.maven.plugins.annotations.Component;
32 import org.apache.maven.plugins.annotations.Mojo;
33 import org.apache.maven.plugins.annotations.Parameter;
34 import org.apache.maven.project.MavenProject;
35 import org.apache.maven.settings.Settings;
36 import org.apache.maven.shared.invoker.DefaultInvocationRequest;
37 import org.apache.maven.shared.invoker.InvocationRequest;
38 import org.apache.maven.shared.invoker.InvocationResult;
39 import org.apache.maven.shared.invoker.Invoker;
40 import org.apache.maven.shared.invoker.MavenInvocationException;
41 import org.apache.maven.shared.scriptinterpreter.RunFailureException;
42 import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
43 import org.codehaus.plexus.util.FileUtils;
44 import org.codehaus.plexus.util.IOUtil;
45 import org.codehaus.plexus.util.InterpolationFilterReader;
46 import org.codehaus.plexus.util.ReaderFactory;
47 import org.codehaus.plexus.util.StringUtils;
48 import org.codehaus.plexus.util.WriterFactory;
49 import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
50
51 import java.io.BufferedReader;
52 import java.io.File;
53 import java.io.FileInputStream;
54 import java.io.FileNotFoundException;
55 import java.io.FileReader;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.io.Reader;
59 import java.io.StringWriter;
60 import java.io.Writer;
61 import java.util.Arrays;
62 import java.util.Collection;
63 import java.util.HashMap;
64 import java.util.LinkedHashMap;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Properties;
68 import java.util.Set;
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 @Mojo( name = "integration-test", requiresProject = true )
93 public class IntegrationTestMojo
94 extends AbstractMojo
95 {
96
97 @Component
98 private ArchetypeGenerator archetypeGenerator;
99
100 @Component
101 private Invoker invoker;
102
103
104
105
106 @Parameter( defaultValue = "${project}", readonly = true, required = true )
107 private MavenProject project;
108
109
110
111
112 @Parameter( property = "archetype.test.skip" )
113 private boolean skip = false;
114
115
116
117
118
119
120 @Parameter( property = "archetype.test.projectsDirectory", defaultValue = "${project.build.testOutputDirectory}/projects", required = true )
121 private File testProjectsDirectory;
122
123
124
125
126
127
128
129
130
131
132 @Parameter( property = "archetype.test.verifyScript", defaultValue = "verify" )
133 private String postBuildHookScript;
134
135
136
137
138
139
140 @Parameter( property = "archetype.test.noLog", defaultValue = "false" )
141 private boolean noLog;
142
143
144
145
146
147
148 @Parameter( property = "archetype.test.streamLogs", defaultValue = "true" )
149 private boolean streamLogs;
150
151
152
153
154
155
156 @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
157 private String encoding;
158
159
160
161
162
163
164 @Parameter( property = "archetype.test.localRepositoryPath", defaultValue = "${settings.localRepository}", required = true )
165 private File localRepositoryPath;
166
167
168
169
170
171
172 @Parameter( property = "archetype.test.showVersion", defaultValue = "false" )
173 private boolean showVersion;
174
175
176
177
178
179
180 @Parameter( property = "archetype.test.ignoreEOLStyle", defaultValue = "false" )
181 private boolean ignoreEOLStyle;
182
183
184
185
186
187
188 @Parameter( property = "archetype.test.debug", defaultValue = "false" )
189 private boolean debug;
190
191
192
193
194
195
196 @Parameter
197 private Map<String, String> filterProperties;
198
199
200
201
202
203
204 @Parameter( defaultValue = "${settings}", required = true, readonly = true )
205 private Settings settings;
206
207
208
209
210
211
212
213
214 @Parameter( property = "archetype.test.settingsFile" )
215 private File settingsFile;
216
217 public void execute()
218 throws MojoExecutionException, MojoFailureException
219 {
220 if ( skip )
221 {
222 return;
223 }
224
225 if ( !testProjectsDirectory.exists() )
226 {
227 getLog().warn( "No Archetype IT projects: root 'projects' directory not found." );
228
229 return;
230 }
231
232 File archetypeFile = project.getArtifact().getFile();
233
234 if ( archetypeFile == null )
235 {
236 throw new MojoFailureException( "Unable to get the archetypes' artifact which should have just been built:"
237 + " you probably launched 'mvn archetype:integration-test' instead of"
238 + " 'mvn integration-test'." );
239 }
240
241 try
242 {
243 List<File> projectsGoalFiles = FileUtils.getFiles( testProjectsDirectory, "*/goal.txt", "" );
244
245 if ( projectsGoalFiles.size() == 0 )
246 {
247 getLog().warn( "No Archetype IT projects: no directory with goal.txt found." );
248
249 return;
250 }
251
252 StringWriter errorWriter = new StringWriter();
253 for ( File goalFile : projectsGoalFiles )
254 {
255 try
256 {
257 processIntegrationTest( goalFile, archetypeFile );
258 }
259 catch ( IntegrationTestFailure ex )
260 {
261 errorWriter.write( "\nArchetype IT '" + goalFile.getParentFile().getName() + "' failed: " );
262 errorWriter.write( ex.getMessage() );
263 }
264 }
265
266 String errors = errorWriter.toString();
267 if ( !StringUtils.isEmpty( errors ) )
268 {
269 throw new MojoExecutionException( errors );
270 }
271 }
272 catch ( IOException ex )
273 {
274 throw new MojoFailureException( ex, ex.getMessage(), ex.getMessage() );
275 }
276 }
277
278
279
280
281
282
283
284
285 private void assertDirectoryEquals( File reference, File actual )
286 throws IntegrationTestFailure, IOException
287 {
288 List<String> referenceFiles =
289 FileUtils.getFileAndDirectoryNames( reference, "**", null, false, true, true, true );
290 getLog().debug( "reference content: " + referenceFiles );
291
292 List<String> actualFiles = FileUtils.getFileAndDirectoryNames( actual, "**", null, false, true, true, true );
293 getLog().debug( "actual content: " + referenceFiles );
294
295 boolean fileNamesEquals = CollectionUtils.isEqualCollection( referenceFiles, actualFiles );
296
297 if ( !fileNamesEquals )
298 {
299 getLog().debug( "Actual list of files is not the same as reference:" );
300 int missing = 0;
301 for ( String ref : referenceFiles )
302 {
303 if ( actualFiles.contains( ref ) )
304 {
305 actualFiles.remove( ref );
306 getLog().debug( "Contained " + ref );
307 }
308 else
309 {
310 missing++;
311 getLog().error( "Not contained " + ref );
312 }
313 }
314 getLog().error( "Remains " + actualFiles );
315
316 throw new IntegrationTestFailure(
317 "Reference and generated project differs (missing: " + missing + ", unexpected: " + actualFiles.size()
318 + ")" );
319 }
320
321 boolean contentEquals = true;
322
323 for ( String file : referenceFiles )
324 {
325 File referenceFile = new File( reference, file );
326 File actualFile = new File( actual, file );
327
328 if ( referenceFile.isDirectory() )
329 {
330 if ( actualFile.isFile() )
331 {
332 getLog().warn( "File " + file + " is a directory in the reference but a file in actual" );
333 contentEquals = false;
334 }
335 }
336 else if ( actualFile.isDirectory() )
337 {
338 if ( referenceFile.isFile() )
339 {
340 getLog().warn( "File " + file + " is a file in the reference but a directory in actual" );
341 contentEquals = false;
342 }
343 }
344 else if ( !contentEquals( referenceFile, actualFile ) )
345 {
346 getLog().warn( "Contents of file " + file + " are not equal" );
347 contentEquals = false;
348 }
349 }
350 if ( !contentEquals )
351 {
352 throw new IntegrationTestFailure( "Some content are not equals" );
353 }
354 }
355
356
357
358
359
360 private boolean contentEquals( File referenceFile, File actualFile )
361 throws IOException
362 {
363
364 if ( !ignoreEOLStyle )
365 {
366 getLog().warn( "Property ignoreEOLStyle was not set - files will be compared considering their EOL style!" );
367 return FileUtils.contentEquals( referenceFile, actualFile );
368 }
369
370 getLog().debug( "Comparing files with EOL style ignored." );
371 BufferedReader referenceFileReader = null;
372 BufferedReader actualFileReader = null;
373 try
374 {
375 referenceFileReader = new BufferedReader( new FileReader( referenceFile ) );
376 actualFileReader = new BufferedReader( new FileReader( actualFile ) );
377
378 String refLine = null;
379 String actualLine = null;
380
381 do
382 {
383 refLine = referenceFileReader.readLine();
384 actualLine = actualFileReader.readLine();
385 if ( !StringUtils.equals( refLine, actualLine ) )
386 {
387 return false;
388 }
389 }
390 while ( refLine != null || actualLine != null );
391
392 return true;
393 }
394 finally
395 {
396 IOUtil.close( referenceFileReader );
397 IOUtil.close( actualFileReader );
398 }
399 }
400
401 private Properties loadProperties( final File propertiesFile )
402 throws IOException, FileNotFoundException
403 {
404 Properties properties = new Properties();
405
406 InputStream in = null;
407 try
408 {
409 in = new FileInputStream( propertiesFile );
410
411 properties.load( in );
412 }
413 finally
414 {
415 IOUtil.close( in );
416 }
417
418 return properties;
419 }
420
421 private void processIntegrationTest( File goalFile, File archetypeFile )
422 throws IntegrationTestFailure, MojoExecutionException
423 {
424 getLog().info( "Processing Archetype IT project: " + goalFile.getParentFile().getName() );
425
426 try
427 {
428 Properties properties = getProperties( goalFile );
429
430 String basedir = goalFile.getParentFile().getPath() + "/project";
431
432 FileUtils.deleteDirectory( basedir );
433
434 FileUtils.mkdir( basedir );
435
436
437 ArchetypeGenerationRequest request =
438 new ArchetypeGenerationRequest().setArchetypeGroupId( project.getGroupId() ).setArchetypeArtifactId(
439 project.getArtifactId() ).setArchetypeVersion( project.getVersion() ).setGroupId(
440 properties.getProperty( Constants.GROUP_ID ) ).setArtifactId(
441 properties.getProperty( Constants.ARTIFACT_ID ) ).setVersion(
442 properties.getProperty( Constants.VERSION ) ).setPackage(
443 properties.getProperty( Constants.PACKAGE ) ).setOutputDirectory( basedir ).setProperties(
444 properties );
445
446
447 ArchetypeGenerationResult result = new ArchetypeGenerationResult();
448
449 archetypeGenerator.generateArchetype( request, archetypeFile, result );
450
451 if ( result.getCause() != null )
452 {
453 if ( result.getCause() instanceof ArchetypeNotConfigured )
454 {
455 ArchetypeNotConfigured anc = (ArchetypeNotConfigured) result.getCause();
456
457 throw new IntegrationTestFailure(
458 "Missing required properties in archetype.properties: " + StringUtils.join(
459 anc.getMissingProperties().iterator(), ", " ), anc );
460 }
461
462 throw new IntegrationTestFailure( result.getCause().getMessage(), result.getCause() );
463 }
464
465 File reference = new File( goalFile.getParentFile(), "reference" );
466
467 if ( reference.exists() )
468 {
469
470 getLog().info( "Comparing generated project with reference content: " + reference );
471
472 assertDirectoryEquals( reference, new File( basedir, request.getArtifactId() ) );
473 }
474
475 String goals = FileUtils.fileRead( goalFile );
476
477 if ( StringUtils.isNotEmpty( goals ) )
478 {
479 invokePostArchetypeGenerationGoals( goals.trim(), new File( basedir, request.getArtifactId() ),
480 goalFile );
481 }
482 }
483 catch ( IOException ioe )
484 {
485 throw new IntegrationTestFailure( ioe );
486 }
487 }
488
489 private Properties getProperties( File goalFile )
490 throws IOException
491 {
492 File propertiesFile = new File( goalFile.getParentFile(), "archetype.properties" );
493
494 return loadProperties( propertiesFile );
495 }
496
497 private void invokePostArchetypeGenerationGoals( String goals, File basedir, File goalFile )
498 throws IntegrationTestFailure, IOException, MojoExecutionException
499 {
500 FileLogger logger = setupLogger( basedir );
501
502 if ( !StringUtils.isBlank( goals ) )
503 {
504
505 getLog().info( "Invoking post-archetype-generation goals: " + goals );
506
507 if ( !localRepositoryPath.exists() )
508 {
509 localRepositoryPath.mkdirs();
510 }
511
512
513 InvocationRequest request = new DefaultInvocationRequest().setBaseDirectory( basedir ).setGoals(
514 Arrays.asList( StringUtils.split( goals, "," ) ) ).setLocalRepositoryDirectory(
515 localRepositoryPath ).setInteractive( false ).setShowErrors( true );
516
517
518 request.setDebug( debug );
519
520 request.setShowVersion( showVersion );
521
522 if ( logger != null )
523 {
524 request.setErrorHandler( logger );
525
526 request.setOutputHandler( logger );
527 }
528
529 File interpolatedSettingsFile = null;
530 if ( settingsFile != null )
531 {
532 File interpolatedSettingsDirectory =
533 new File( project.getBuild().getOutputDirectory(), "archetype-it" );
534 if ( interpolatedSettingsDirectory.exists() )
535 {
536 FileUtils.deleteDirectory( interpolatedSettingsDirectory );
537 }
538 interpolatedSettingsDirectory.mkdir();
539 interpolatedSettingsFile =
540 new File( interpolatedSettingsDirectory, "interpolated-" + settingsFile.getName() );
541
542 buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
543
544 request.setUserSettingsFile( interpolatedSettingsFile );
545 }
546
547 try
548 {
549 InvocationResult result = invoker.execute( request );
550
551 getLog().info( "Post-archetype-generation invoker exit code: " + result.getExitCode() );
552
553 if ( result.getExitCode() != 0 )
554 {
555 throw new IntegrationTestFailure( "Execution failure: exit code = " + result.getExitCode(),
556 result.getExecutionException() );
557 }
558 }
559 catch ( MavenInvocationException e )
560 {
561 throw new IntegrationTestFailure( "Cannot run additions goals.", e );
562 }
563 }
564 else
565 {
566 getLog().info( "No post-archetype-generation goals to invoke." );
567 }
568
569 ScriptRunner scriptRunner = new ScriptRunner( getLog() );
570 scriptRunner.setScriptEncoding( encoding );
571
572 Map<String, Object> context = new LinkedHashMap<String, Object>();
573 context.put( "projectDir", basedir );
574
575 try
576 {
577 scriptRunner.run( "post-build script", goalFile.getParentFile(), postBuildHookScript, context, logger,
578 "failure post script", true );
579 }
580 catch ( RunFailureException e )
581 {
582 throw new IntegrationTestFailure( "post build script failure failure: " + e.getMessage(), e );
583 }
584 }
585
586 private FileLogger setupLogger( File basedir )
587 throws IOException
588 {
589 FileLogger logger = null;
590
591 if ( !noLog )
592 {
593 File outputLog = new File( basedir, "build.log" );
594
595 if ( streamLogs )
596 {
597 logger = new FileLogger( outputLog, getLog() );
598 }
599 else
600 {
601 logger = new FileLogger( outputLog );
602 }
603
604 getLog().debug( "build log initialized in: " + outputLog );
605
606 }
607
608 return logger;
609 }
610
611 class IntegrationTestFailure
612 extends Exception
613 {
614 IntegrationTestFailure()
615 {
616 super();
617 }
618
619 IntegrationTestFailure( String message )
620 {
621 super( message );
622 }
623
624 IntegrationTestFailure( Throwable cause )
625 {
626 super( cause );
627 }
628
629 IntegrationTestFailure( String message, Throwable cause )
630 {
631 super( message, cause );
632 }
633 }
634
635
636
637
638
639
640 private Map<String, Object> getInterpolationValueSource()
641 {
642 Map<String, Object> props = new HashMap<String, Object>();
643 if ( filterProperties != null )
644 {
645 props.putAll( filterProperties );
646 }
647 if ( filterProperties != null )
648 {
649 props.putAll( filterProperties );
650 }
651 props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
652 props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
653 if ( settings.getLocalRepository() != null )
654 {
655 props.put( "localRepository", settings.getLocalRepository() );
656 props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
657 }
658 return new CompositeMap( this.project, props );
659 }
660
661 protected void buildInterpolatedFile( File originalFile, File interpolatedFile )
662 throws MojoExecutionException
663 {
664 getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
665
666 try
667 {
668 String xml;
669
670 Reader reader = null;
671 try
672 {
673
674 Map<String, Object> composite = getInterpolationValueSource();
675 reader = ReaderFactory.newXmlReader( originalFile );
676 reader = new InterpolationFilterReader( reader, composite, "@", "@" );
677 xml = IOUtil.toString( reader );
678 }
679 finally
680 {
681 IOUtil.close( reader );
682 }
683
684 Writer writer = null;
685 try
686 {
687 interpolatedFile.getParentFile().mkdirs();
688 writer = WriterFactory.newXmlWriter( interpolatedFile );
689 writer.write( xml );
690 writer.flush();
691 }
692 finally
693 {
694 IOUtil.close( writer );
695 }
696 }
697 catch ( IOException e )
698 {
699 throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
700 }
701 }
702
703 private static class CompositeMap
704 implements Map<String, Object>
705 {
706
707
708
709
710 private MavenProject mavenProject;
711
712
713
714
715 private Map<String, Object> properties;
716
717
718
719
720
721
722
723
724
725 protected CompositeMap( MavenProject mavenProject, Map<String, Object> properties )
726 {
727 if ( mavenProject == null )
728 {
729 throw new IllegalArgumentException( "no project specified" );
730 }
731 this.mavenProject = mavenProject;
732 this.properties = properties == null ? (Map) new Properties() : properties;
733 }
734
735
736
737
738
739
740 public void clear()
741 {
742
743 }
744
745
746
747
748
749
750 public boolean containsKey( Object key )
751 {
752 if ( !( key instanceof String ) )
753 {
754 return false;
755 }
756
757 String expression = (String) key;
758 if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
759 {
760 try
761 {
762 Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
763 if ( evaluated != null )
764 {
765 return true;
766 }
767 }
768 catch ( Exception e )
769 {
770
771 }
772 }
773
774 return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
775 }
776
777
778
779
780
781
782 public boolean containsValue( Object value )
783 {
784 throw new UnsupportedOperationException();
785 }
786
787
788
789
790
791
792 public Set<Entry<String, Object>> entrySet()
793 {
794 throw new UnsupportedOperationException();
795 }
796
797
798
799
800
801
802 public Object get( Object key )
803 {
804 if ( !( key instanceof String ) )
805 {
806 return null;
807 }
808
809 String expression = (String) key;
810 if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
811 {
812 try
813 {
814 Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
815 if ( evaluated != null )
816 {
817 return evaluated;
818 }
819 }
820 catch ( Exception e )
821 {
822
823 }
824 }
825
826 Object value = properties.get( key );
827
828 return ( value != null ? value : this.mavenProject.getProperties().get( key ) );
829
830 }
831
832
833
834
835
836
837 public boolean isEmpty()
838 {
839 return this.mavenProject == null && this.mavenProject.getProperties().isEmpty()
840 && this.properties.isEmpty();
841 }
842
843
844
845
846
847
848 public Set<String> keySet()
849 {
850 throw new UnsupportedOperationException();
851 }
852
853
854
855
856
857
858 public Object put( String key, Object value )
859 {
860 throw new UnsupportedOperationException();
861 }
862
863
864
865
866
867
868 public void putAll( Map<? extends String, ? extends Object> t )
869 {
870 throw new UnsupportedOperationException();
871 }
872
873
874
875
876
877
878 public Object remove( Object key )
879 {
880 throw new UnsupportedOperationException();
881 }
882
883
884
885
886
887
888 public int size()
889 {
890 throw new UnsupportedOperationException();
891 }
892
893
894
895
896
897
898 public Collection<Object> values()
899 {
900 throw new UnsupportedOperationException();
901 }
902 }
903
904
905
906
907
908
909
910
911 private static String toUrl( String filename )
912 {
913
914
915
916
917 String url = "file://" + new File( filename ).toURI().getPath();
918 if ( url.endsWith( "/" ) )
919 {
920 url = url.substring( 0, url.length() - 1 );
921 }
922 return url;
923 }
924 }