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