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.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   * <p>
70   * Execute the archetype integration tests, consisting in generating projects from the current archetype and optionally
71   * comparing generated projects with reference copy.
72   * </p>
73   * <p/>
74   * <p>
75   * Each IT consists of a sub-directory in <code>src/test/resources/projects</code> containing:
76   * </p>
77   * <ul>
78   * <li>a <code>goal.txt</code> file, containing a list of goals to run against the generated project (can be empty,
79   * content ignored before maven-archetype-plugin 2.1),</li>
80   * <li>an <code>archetype.properties</code> file, containing properties for project generation,</li>
81   * <li>an optional <code>reference/</code> directory containing a reference copy of the expected project created from
82   * the IT.</li>
83   * </ul>
84   * <p/>
85   * 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
86   * goal from CLI.
87   *
88   * @author rafale
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      * The archetype project to execute the integration tests on.
103      */
104     @Parameter( defaultValue = "${project}", readonly = true, required = true )
105     private MavenProject project;
106 
107     /**
108      * Skip the integration test.
109      */
110     @Parameter( property = "archetype.test.skip" )
111     private boolean skip = false;
112 
113     /**
114      * Directory of test projects
115      *
116      * @since 2.2
117      */
118     @Parameter( property = "archetype.test.projectsDirectory", defaultValue = "${project.build.testOutputDirectory}/projects", required = true )
119     private File testProjectsDirectory;
120 
121     /**
122      * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
123      * with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>verify</code>), the
124      * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
125      * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
126      * throws an exception, the corresponding build is flagged as a failure.
127      *
128      * @since 2.2
129      */
130     @Parameter( property = "archetype.test.verifyScript", defaultValue = "verify" )
131     private String postBuildHookScript;
132 
133     /**
134      * Suppress logging to the <code>build.log</code> file.
135      *
136      * @since 2.2
137      */
138     @Parameter( property = "archetype.test.noLog", defaultValue = "false" )
139     private boolean noLog;
140 
141     /**
142      * Flag used to determine whether the build logs should be output to the normal mojo log.
143      *
144      * @since 2.2
145      */
146     @Parameter( property = "archetype.test.streamLogs", defaultValue = "true" )
147     private boolean streamLogs;
148 
149     /**
150      * The file encoding for the post-build script.
151      *
152      * @since 2.2
153      */
154     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
155     private String encoding;
156 
157     /**
158      * The local repository to run maven instance.
159      *
160      * @since 2.2
161      */
162     @Parameter( property = "archetype.test.localRepositoryPath", defaultValue = "${settings.localRepository}", required = true )
163     private File localRepositoryPath;
164 
165     /**
166      * flag to enable show mvn version used for running its (cli option : -V,--show-version )
167      *
168      * @since 2.2
169      */
170     @Parameter( property = "archetype.test.showVersion", defaultValue = "false" )
171     private boolean showVersion;
172 
173     /**
174      * Whether to show debug statements in the build output.
175      *
176      * @since 2.2
177      */
178     @Parameter( property = "archetype.test.debug", defaultValue = "false" )
179     private boolean debug;
180 
181     /**
182      * A list of additional properties which will be used to filter tokens in settings.xml
183      *
184      * @since 2.2
185      */
186     @Parameter
187     private Map<String, String> filterProperties;
188 
189     /**
190      * The current user system settings for use in Maven.
191      *
192      * @since 2.2
193      */
194     @Parameter( defaultValue = "${settings}", required = true, readonly = true )
195     private Settings settings;
196 
197     /**
198      * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
199      * <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
200      * parameter {@link #localRepositoryPath} is dominant.
201      *
202      * @since 2.2
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      * Checks that actual directory content is the same as reference.
269      *
270      * @param reference the reference directory
271      * @param actual the actual directory to compare with the reference
272      * @throws IntegrationTestFailure if content differs
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             //@formatter:off
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             //@formatter:on
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                 // compare generated project with reference
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             //@formatter:off
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             //@formatter:on
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         // verify result
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      * Returns the map-based value source used to interpolate settings and other stuff.
580      *
581      * @return The map-based value source for interpolation, never <code>null</code>.
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                 // interpolation with token @...@
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          * The Maven project from which to extract interpolated values, never <code>null</code>.
652          */
653         private MavenProject mavenProject;
654 
655         /**
656          * The set of additional properties from which to extract interpolated values, never <code>null</code>.
657          */
658         private Map<String, Object> properties;
659 
660         /**
661          * Creates a new interpolation source backed by the specified Maven project and some user-specified properties.
662          *
663          * @param mavenProject The Maven project from which to extract interpolated values, must not be
664          *            <code>null</code>.
665          * @param properties The set of additional properties from which to extract interpolated values, may be
666          *            <code>null</code>.
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          * {@inheritDoc}
680          *
681          * @see java.util.Map#clear()
682          */
683         public void clear()
684         {
685             // nothing here
686         }
687 
688         /**
689          * {@inheritDoc}
690          *
691          * @see java.util.Map#containsKey(java.lang.Object)
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                     // uhm do we have to throw a RuntimeException here ?
714                 }
715             }
716 
717             return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
718         }
719 
720         /**
721          * {@inheritDoc}
722          *
723          * @see java.util.Map#containsValue(java.lang.Object)
724          */
725         public boolean containsValue( Object value )
726         {
727             throw new UnsupportedOperationException();
728         }
729 
730         /**
731          * {@inheritDoc}
732          *
733          * @see java.util.Map#entrySet()
734          */
735         public Set<Entry<String, Object>> entrySet()
736         {
737             throw new UnsupportedOperationException();
738         }
739 
740         /**
741          * {@inheritDoc}
742          *
743          * @see java.util.Map#get(java.lang.Object)
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                     // uhm do we have to throw a RuntimeException here ?
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          * {@inheritDoc}
777          *
778          * @see java.util.Map#isEmpty()
779          */
780         public boolean isEmpty()
781         {
782             return this.mavenProject == null && this.mavenProject.getProperties().isEmpty()
783                 && this.properties.isEmpty();
784         }
785 
786         /**
787          * {@inheritDoc}
788          *
789          * @see java.util.Map#keySet()
790          */
791         public Set<String> keySet()
792         {
793             throw new UnsupportedOperationException();
794         }
795 
796         /**
797          * {@inheritDoc}
798          *
799          * @see java.util.Map#put(java.lang.Object, java.lang.Object)
800          */
801         public Object put( String key, Object value )
802         {
803             throw new UnsupportedOperationException();
804         }
805 
806         /**
807          * {@inheritDoc}
808          *
809          * @see java.util.Map#putAll(java.util.Map)
810          */
811         public void putAll( Map<? extends String, ? extends Object> t )
812         {
813             throw new UnsupportedOperationException();
814         }
815 
816         /**
817          * {@inheritDoc}
818          *
819          * @see java.util.Map#remove(java.lang.Object)
820          */
821         public Object remove( Object key )
822         {
823             throw new UnsupportedOperationException();
824         }
825 
826         /**
827          * {@inheritDoc}
828          *
829          * @see java.util.Map#size()
830          */
831         public int size()
832         {
833             throw new UnsupportedOperationException();
834         }
835 
836         /**
837          * {@inheritDoc}
838          *
839          * @see java.util.Map#values()
840          */
841         public Collection<Object> values()
842         {
843             throw new UnsupportedOperationException();
844         }
845     }
846 
847     /**
848      * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
849      * path denotes a file or a directory.
850      *
851      * @param filename The filesystem path to convert, must not be <code>null</code>.
852      * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
853      */
854     private static String toUrl( String filename )
855     {
856         /*
857          * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
858          * as-is but use the decoded path component in the URL.
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 }