View Javadoc
1   package org.apache.maven.archetype.mojos;
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 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   * <p>
72   * Execute the archetype integration tests, consisting in generating projects from the current archetype and optionally
73   * comparing generated projects with reference copy.
74   * </p>
75   * <p/>
76   * <p>
77   * Each IT consists of a sub-directory in <code>src/test/resources/projects</code> containing:
78   * </p>
79   * <ul>
80   * <li>a <code>goal.txt</code> file, containing a list of goals to run against the generated project (can be empty,
81   * content ignored before maven-archetype-plugin 2.1),</li>
82   * <li>an <code>archetype.properties</code> file, containing properties for project generation,</li>
83   * <li>an optional <code>reference/</code> directory containing a reference copy of the expected project created from
84   * the IT.</li>
85   * </ul>
86   * <p/>
87   * Notice that it is expected to be run as part as of a build after the <code>package</code> phase and not directly as a
88   * goal from CLI.
89   *
90   * @author rafale
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      * The archetype project to execute the integration tests on.
105      */
106     @Parameter( defaultValue = "${project}", readonly = true, required = true )
107     private MavenProject project;
108 
109     /**
110      * Skip the integration test.
111      */
112     @Parameter( property = "archetype.test.skip" )
113     private boolean skip = false;
114 
115     /**
116      * Directory of test projects
117      *
118      * @since 2.2
119      */
120     @Parameter( property = "archetype.test.projectsDirectory", defaultValue = "${project.build.testOutputDirectory}/projects", required = true )
121     private File testProjectsDirectory;
122 
123     /**
124      * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
125      * with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>verify</code>), the
126      * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
127      * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
128      * throws an exception, the corresponding build is flagged as a failure.
129      *
130      * @since 2.2
131      */
132     @Parameter( property = "archetype.test.verifyScript", defaultValue = "verify" )
133     private String postBuildHookScript;
134 
135     /**
136      * Suppress logging to the <code>build.log</code> file.
137      *
138      * @since 2.2
139      */
140     @Parameter( property = "archetype.test.noLog", defaultValue = "false" )
141     private boolean noLog;
142 
143     /**
144      * Flag used to determine whether the build logs should be output to the normal mojo log.
145      *
146      * @since 2.2
147      */
148     @Parameter( property = "archetype.test.streamLogs", defaultValue = "true" )
149     private boolean streamLogs;
150 
151     /**
152      * The file encoding for the post-build script.
153      *
154      * @since 2.2
155      */
156     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
157     private String encoding;
158 
159     /**
160      * The local repository to run maven instance.
161      *
162      * @since 2.2
163      */
164     @Parameter( property = "archetype.test.localRepositoryPath", defaultValue = "${settings.localRepository}", required = true )
165     private File localRepositoryPath;
166 
167     /**
168      * flag to enable show mvn version used for running its (cli option : -V,--show-version )
169      *
170      * @since 2.2
171      */
172     @Parameter( property = "archetype.test.showVersion", defaultValue = "false" )
173     private boolean showVersion;
174 
175     /**
176      * Ignores the EOL encoding for comparing files (default and original behaviour is false).
177      *
178      * @since 2.3
179      */
180     @Parameter( property = "archetype.test.ignoreEOLStyle", defaultValue = "false" )
181     private boolean ignoreEOLStyle;
182 
183     /**
184      * Whether to show debug statements in the build output.
185      *
186      * @since 2.2
187      */
188     @Parameter( property = "archetype.test.debug", defaultValue = "false" )
189     private boolean debug;
190 
191     /**
192      * A list of additional properties which will be used to filter tokens in settings.xml
193      *
194      * @since 2.2
195      */
196     @Parameter
197     private Map<String, String> filterProperties;
198 
199     /**
200      * The current user system settings for use in Maven.
201      *
202      * @since 2.2
203      */
204     @Parameter( defaultValue = "${settings}", required = true, readonly = true )
205     private Settings settings;
206 
207     /**
208      * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
209      * <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
210      * parameter {@link #localRepositoryPath} is dominant.
211      *
212      * @since 2.2
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      * Checks that actual directory content is the same as reference.
280      *
281      * @param reference the reference directory
282      * @param actual    the actual directory to compare with the reference
283      * @throws IntegrationTestFailure if content differs
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      * Uses the {@link #ignoreEOLStyle} attribute to compare the two files. If {@link #ignoreEOLStyle} is true,
358      * then the comparison does not take care about the EOL (aka newline) character.
359      */
360     private boolean contentEquals( File referenceFile, File actualFile )
361         throws IOException
362     {
363         // Original behaviour
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             //@formatter:off
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             //@formatter:on
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                 // compare generated project with reference
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             //@formatter:off
513             InvocationRequest request = new DefaultInvocationRequest().setBaseDirectory( basedir ).setGoals(
514                 Arrays.asList( StringUtils.split( goals, "," ) ) ).setLocalRepositoryDirectory(
515                 localRepositoryPath ).setInteractive( false ).setShowErrors( true );
516             //@formatter:on
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         // verify result
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      * Returns the map-based value source used to interpolate settings and other stuff.
637      *
638      * @return The map-based value source for interpolation, never <code>null</code>.
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                 // interpolation with token @...@
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          * The Maven project from which to extract interpolated values, never <code>null</code>.
709          */
710         private MavenProject mavenProject;
711 
712         /**
713          * The set of additional properties from which to extract interpolated values, never <code>null</code>.
714          */
715         private Map<String, Object> properties;
716 
717         /**
718          * Creates a new interpolation source backed by the specified Maven project and some user-specified properties.
719          *
720          * @param mavenProject The Maven project from which to extract interpolated values, must not be
721          *                     <code>null</code>.
722          * @param properties   The set of additional properties from which to extract interpolated values, may be
723          *                     <code>null</code>.
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          * {@inheritDoc}
737          *
738          * @see java.util.Map#clear()
739          */
740         public void clear()
741         {
742             // nothing here
743         }
744 
745         /**
746          * {@inheritDoc}
747          *
748          * @see java.util.Map#containsKey(java.lang.Object)
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                     // uhm do we have to throw a RuntimeException here ?
771                 }
772             }
773 
774             return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
775         }
776 
777         /**
778          * {@inheritDoc}
779          *
780          * @see java.util.Map#containsValue(java.lang.Object)
781          */
782         public boolean containsValue( Object value )
783         {
784             throw new UnsupportedOperationException();
785         }
786 
787         /**
788          * {@inheritDoc}
789          *
790          * @see java.util.Map#entrySet()
791          */
792         public Set<Entry<String, Object>> entrySet()
793         {
794             throw new UnsupportedOperationException();
795         }
796 
797         /**
798          * {@inheritDoc}
799          *
800          * @see java.util.Map#get(java.lang.Object)
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                     // uhm do we have to throw a RuntimeException here ?
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          * {@inheritDoc}
834          *
835          * @see java.util.Map#isEmpty()
836          */
837         public boolean isEmpty()
838         {
839             return this.mavenProject == null && this.mavenProject.getProperties().isEmpty()
840                 && this.properties.isEmpty();
841         }
842 
843         /**
844          * {@inheritDoc}
845          *
846          * @see java.util.Map#keySet()
847          */
848         public Set<String> keySet()
849         {
850             throw new UnsupportedOperationException();
851         }
852 
853         /**
854          * {@inheritDoc}
855          *
856          * @see java.util.Map#put(java.lang.Object, java.lang.Object)
857          */
858         public Object put( String key, Object value )
859         {
860             throw new UnsupportedOperationException();
861         }
862 
863         /**
864          * {@inheritDoc}
865          *
866          * @see java.util.Map#putAll(java.util.Map)
867          */
868         public void putAll( Map<? extends String, ? extends Object> t )
869         {
870             throw new UnsupportedOperationException();
871         }
872 
873         /**
874          * {@inheritDoc}
875          *
876          * @see java.util.Map#remove(java.lang.Object)
877          */
878         public Object remove( Object key )
879         {
880             throw new UnsupportedOperationException();
881         }
882 
883         /**
884          * {@inheritDoc}
885          *
886          * @see java.util.Map#size()
887          */
888         public int size()
889         {
890             throw new UnsupportedOperationException();
891         }
892 
893         /**
894          * {@inheritDoc}
895          *
896          * @see java.util.Map#values()
897          */
898         public Collection<Object> values()
899         {
900             throw new UnsupportedOperationException();
901         }
902     }
903 
904     /**
905      * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
906      * path denotes a file or a directory.
907      *
908      * @param filename The filesystem path to convert, must not be <code>null</code>.
909      * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
910      */
911     private static String toUrl( String filename )
912     {
913         /*
914          * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
915          * as-is but use the decoded path component in the URL.
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 }