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.project.MavenProject;
32  import org.apache.maven.settings.Settings;
33  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
34  import org.apache.maven.shared.invoker.InvocationRequest;
35  import org.apache.maven.shared.invoker.InvocationResult;
36  import org.apache.maven.shared.invoker.Invoker;
37  import org.apache.maven.shared.invoker.MavenInvocationException;
38  import org.apache.maven.shared.scriptinterpreter.RunFailureException;
39  import org.apache.maven.shared.scriptinterpreter.ScriptRunner;
40  import org.codehaus.plexus.util.FileUtils;
41  import org.codehaus.plexus.util.IOUtil;
42  import org.codehaus.plexus.util.InterpolationFilterReader;
43  import org.codehaus.plexus.util.ReaderFactory;
44  import org.codehaus.plexus.util.StringUtils;
45  import org.codehaus.plexus.util.WriterFactory;
46  import org.codehaus.plexus.util.introspection.ReflectionValueExtractor;
47  
48  import java.io.File;
49  import java.io.FileInputStream;
50  import java.io.FileNotFoundException;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.io.Reader;
54  import java.io.StringWriter;
55  import java.io.Writer;
56  import java.util.Arrays;
57  import java.util.Collection;
58  import java.util.HashMap;
59  import java.util.LinkedHashMap;
60  import java.util.List;
61  import java.util.Map;
62  import java.util.Properties;
63  import java.util.Set;
64  
65  /**
66   * <p>Execute the archetype integration tests, consisting in generating projects from the current archetype and
67   * optionally comparing generated projects with reference copy.</p>
68   * <p/>
69   * <p>Each IT consists of a sub-directory in <code>src/test/resources/projects</code> containing:</p>
70   * <ul>
71   * <li>a <code>goal.txt</code> file, containing a list of goals to run against the generated project (can be empty,
72   * content ignored before maven-archetype-plugin 2.1),</li>
73   * <li>an <code>archetype.properties</code> file, containing properties for project generation,</li>
74   * <li>an optional <code>reference/</code> directory containing a reference copy of the expected project created from the IT.</li>
75   * </ul>
76   * <p/>
77   * Notice that it is expected to be run as part as of a build after the <code>package</code> phase and not directly
78   * as a goal from CLI.
79   *
80   * @author rafale
81   * @requiresProject true
82   * @goal integration-test
83   */
84  public class IntegrationTestMojo
85      extends AbstractMojo
86  {
87      /**
88       * @component
89       */
90      private ArchetypeGenerator archetypeGenerator;
91  
92      /**
93       * @component
94       */
95      private Invoker invoker;
96  
97      /**
98       * The archetype project to execute the integration tests on.
99       *
100      * @parameter expression="${project}"
101      * @required
102      * @readonly
103      */
104     private MavenProject project;
105 
106     /**
107      * Skip the integration test.
108      *
109      * @parameter expression="${archetype.test.skip}"
110      * @readonly
111      */
112     private boolean skip = false;
113 
114 
115     /**
116      * Directory of test projects
117      *
118      * @parameter expression="${archetype.test.projectsDirectory}" default-value="${project.build.testOutputDirectory}/projects"
119      * @required
120      * @since 2.2
121      */
122     private File testProjectsDirectory;
123 
124     /**
125      * Relative path of a cleanup/verification hook script to run after executing the build. This script may be written
126      * with either BeanShell or Groovy. If the file extension is omitted (e.g. <code>verify</code>), the
127      * plugin searches for the file by trying out the well-known extensions <code>.bsh</code> and <code>.groovy</code>.
128      * If this script exists for a particular project but returns any non-null value different from <code>true</code> or
129      * throws an exception, the corresponding build is flagged as a failure.
130      *
131      * @parameter expression="${archetype.test.verifyScript}" default-value="verify"
132      * @since 2.2
133      */
134     private String postBuildHookScript;
135 
136     /**
137      * Suppress logging to the <code>build.log</code> file.
138      *
139      * @parameter expression="${archetype.test.noLog}" default-value="false"
140      * @since 2.2
141      */
142     private boolean noLog;
143 
144     /**
145      * Flag used to determine whether the build logs should be output to the normal mojo log.
146      *
147      * @parameter expression="${archetype.test.streamLogs}" default-value="true"
148      * @since 2.2
149      */
150     private boolean streamLogs;
151 
152     /**
153      * The file encoding for the post-build script.
154      *
155      * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
156      * @since 2.2
157      */
158     private String encoding;
159 
160     /**
161      * The local repository to run maven instance.
162      *
163      * @parameter expression="${archetype.test.localRepositoryPath}" default-value="${settings.localRepository}"
164      * @required
165      * @readonly
166      * @since 2.2
167      */
168     private File localRepositoryPath;
169 
170     /**
171      * flag to enable show mvn version used for running its (cli option : -V,--show-version )
172      *
173      * @parameter expression="${archetype.test.showVersion}" default-value="false"
174      * @since 2.2
175      */
176     private boolean showVersion;
177 
178     /**
179      * Whether to show debug statements in the build output.
180      *
181      * @parameter expression="${archetype.test.debug}" default-value="false"
182      * @since 2.2
183      */
184     private boolean debug;
185 
186     /**
187      * A list of additional properties which will be used to filter tokens in settings.xml
188      *
189      * @parameter
190      * @since 2.2
191      */
192     private Map<String, String> filterProperties;
193 
194     /**
195      * The current user system settings for use in Maven.
196      *
197      * @parameter expression="${settings}"
198      * @required
199      * @readonly
200      * @since 2.2
201      */
202     private Settings settings;
203 
204     /**
205      * Path to an alternate <code>settings.xml</code> to use for Maven invocation with all ITs. Note that the
206      * <code>&lt;localRepository&gt;</code> element of this settings file is always ignored, i.e. the path given by the
207      * parameter {@link #localRepositoryPath} is dominant.
208      *
209      * @parameter expression="${archetype.test.settingsFile}"
210      * @since 2.2
211      */
212     private File settingsFile;
213 
214 
215     public void execute()
216         throws MojoExecutionException, MojoFailureException
217     {
218         if ( skip )
219         {
220             return;
221         }
222 
223         if ( !testProjectsDirectory.exists() )
224         {
225             getLog().warn( "No Archetype IT projects: root 'projects' directory not found." );
226 
227             return;
228         }
229 
230         File archetypeFile = project.getArtifact().getFile();
231 
232         if ( archetypeFile == null )
233         {
234             throw new MojoFailureException( "Unable to get the archetypes' artifact which should have just been built:"
235                                                 + " you probably launched 'mvn archetype:integration-test' instead of"
236                                                 + " 'mvn integration-test'." );
237         }
238 
239         try
240         {
241             @SuppressWarnings( "unchecked" ) List<File> projectsGoalFiles =
242                 FileUtils.getFiles( testProjectsDirectory, "*/goal.txt", "" );
243 
244             if ( projectsGoalFiles.size() == 0 )
245             {
246                 getLog().warn( "No Archetype IT projects: no directory with goal.txt found." );
247 
248                 return;
249             }
250 
251             StringWriter errorWriter = new StringWriter();
252             for ( File goalFile : projectsGoalFiles )
253             {
254                 try
255                 {
256                     processIntegrationTest( goalFile, archetypeFile );
257                 }
258                 catch ( IntegrationTestFailure ex )
259                 {
260                     errorWriter.write( "\nArchetype IT '" + goalFile.getParentFile().getName() + "' failed: " );
261                     errorWriter.write( ex.getMessage() );
262                 }
263             }
264 
265             String errors = errorWriter.toString();
266             if ( !StringUtils.isEmpty( errors ) )
267             {
268                 throw new MojoExecutionException( errors );
269             }
270         }
271         catch ( IOException ex )
272         {
273             throw new MojoFailureException( ex, ex.getMessage(), ex.getMessage() );
274         }
275     }
276 
277     /**
278      * Checks that actual directory content is the same as reference.
279      *
280      * @param reference the reference directory
281      * @param actual    the actual directory to compare with the reference
282      * @throws IntegrationTestFailure if content differs
283      */
284     private void assertDirectoryEquals( File reference, File actual )
285         throws IntegrationTestFailure, IOException
286     {
287         @SuppressWarnings( "unchecked" ) List<String> referenceFiles =
288             FileUtils.getFileAndDirectoryNames( reference, "**", null, false, true, true, true );
289         getLog().debug( "reference content: " + referenceFiles );
290 
291         @SuppressWarnings( "unchecked" ) List<String> actualFiles =
292             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 ( !FileUtils.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     private Properties loadProperties( final File propertiesFile )
357         throws IOException, FileNotFoundException
358     {
359         Properties properties = new Properties();
360 
361         InputStream in = null;
362         try
363         {
364             in = new FileInputStream( propertiesFile );
365 
366             properties.load( in );
367         }
368         finally
369         {
370             IOUtil.close( in );
371         }
372 
373         return properties;
374     }
375 
376     private void processIntegrationTest( File goalFile, File archetypeFile )
377         throws IntegrationTestFailure, MojoExecutionException
378     {
379         getLog().info( "Processing Archetype IT project: " + goalFile.getParentFile().getName() );
380 
381         try
382         {
383             Properties properties = getProperties( goalFile );
384 
385             String basedir = goalFile.getParentFile().getPath() + "/project";
386 
387             FileUtils.deleteDirectory( basedir );
388 
389             FileUtils.mkdir( basedir );
390 
391             ArchetypeGenerationRequest request =
392                 new ArchetypeGenerationRequest().setArchetypeGroupId( project.getGroupId() ).setArchetypeArtifactId(
393                     project.getArtifactId() ).setArchetypeVersion( project.getVersion() ).setGroupId(
394                     properties.getProperty( Constants.GROUP_ID ) ).setArtifactId(
395                     properties.getProperty( Constants.ARTIFACT_ID ) ).setVersion(
396                     properties.getProperty( Constants.VERSION ) ).setPackage(
397                     properties.getProperty( Constants.PACKAGE ) ).setOutputDirectory( basedir ).setProperties(
398                     properties );
399 
400             ArchetypeGenerationResult result = new ArchetypeGenerationResult();
401 
402             archetypeGenerator.generateArchetype( request, archetypeFile, result );
403 
404             if ( result.getCause() != null )
405             {
406                 if ( result.getCause() instanceof ArchetypeNotConfigured )
407                 {
408                     ArchetypeNotConfigured anc = (ArchetypeNotConfigured) result.getCause();
409 
410                     throw new IntegrationTestFailure(
411                         "Missing required properties in archetype.properties: " + StringUtils.join(
412                             anc.getMissingProperties().iterator(), ", " ), anc );
413                 }
414 
415                 throw new IntegrationTestFailure( result.getCause().getMessage(), result.getCause() );
416             }
417 
418             File reference = new File( goalFile.getParentFile(), "reference" );
419 
420             if ( reference.exists() )
421             {
422                 // compare generated project with reference
423                 getLog().info( "Comparing generated project with reference content: " + reference );
424 
425                 assertDirectoryEquals( reference, new File( basedir, request.getArtifactId() ) );
426             }
427 
428             String goals = FileUtils.fileRead( goalFile );
429 
430             invokePostArchetypeGenerationGoals( goals, new File( basedir, request.getArtifactId() ), goalFile );
431         }
432         catch ( IOException ioe )
433         {
434             throw new IntegrationTestFailure( ioe );
435         }
436     }
437 
438     private Properties getProperties( File goalFile )
439         throws IOException
440     {
441         File propertiesFile = new File( goalFile.getParentFile(), "archetype.properties" );
442 
443         return loadProperties( propertiesFile );
444     }
445 
446     private void invokePostArchetypeGenerationGoals( String goals, File basedir, File goalFile )
447         throws IntegrationTestFailure, IOException, MojoExecutionException
448     {
449         FileLogger logger = setupLogger( basedir );
450 
451         if ( !StringUtils.isBlank( goals ) )
452         {
453 
454             getLog().info( "Invoking post-archetype-generation goals: " + goals );
455 
456             if ( !localRepositoryPath.exists() )
457             {
458                 localRepositoryPath.mkdirs();
459             }
460 
461             InvocationRequest request = new DefaultInvocationRequest().setBaseDirectory( basedir ).setGoals(
462                 Arrays.asList( StringUtils.split( goals, "," ) ) ).setLocalRepositoryDirectory(
463                 localRepositoryPath ).setInteractive( false ).setShowErrors( true );
464 
465             request.setDebug( debug );
466 
467             request.setShowVersion( showVersion );
468 
469             if ( logger != null )
470             {
471                 request.setErrorHandler( logger );
472 
473                 request.setOutputHandler( logger );
474             }
475 
476             File interpolatedSettingsFile = null;
477             if ( settingsFile != null )
478             {
479                 File interpolatedSettingsDirectory =
480                     new File( project.getBuild().getOutputDirectory(), "archetype-it" );
481                 if ( interpolatedSettingsDirectory.exists() )
482                 {
483                     FileUtils.deleteDirectory( interpolatedSettingsDirectory );
484                 }
485                 interpolatedSettingsDirectory.mkdir();
486                 interpolatedSettingsFile =
487                     new File( interpolatedSettingsDirectory, "interpolated-" + settingsFile.getName() );
488 
489                 buildInterpolatedFile( settingsFile, interpolatedSettingsFile );
490 
491                 request.setUserSettingsFile( interpolatedSettingsFile );
492             }
493 
494             try
495             {
496                 InvocationResult result = invoker.execute( request );
497 
498                 getLog().info( "Post-archetype-generation invoker exit code: " + result.getExitCode() );
499 
500                 if ( result.getExitCode() != 0 )
501                 {
502                     throw new IntegrationTestFailure( "Execution failure: exit code = " + result.getExitCode(),
503                                                       result.getExecutionException() );
504                 }
505             }
506             catch ( MavenInvocationException e )
507             {
508                 throw new IntegrationTestFailure( "Cannot run additions goals.", e );
509             }
510         }
511         else
512         {
513             getLog().info( "No post-archetype-generation goals to invoke." );
514         }
515         // verify result
516         ScriptRunner scriptRunner = new ScriptRunner( getLog() );
517         scriptRunner.setScriptEncoding( encoding );
518 
519         Map<String, Object> context = new LinkedHashMap<String, Object>();
520         context.put( "projectDir", basedir );
521 
522         try
523         {
524             scriptRunner.run( "post-build script", goalFile.getParentFile(), postBuildHookScript, context, logger,
525                               "failure post script", true );
526         }
527         catch ( RunFailureException e )
528         {
529             throw new IntegrationTestFailure( "post build script failure failure: " + e.getMessage(), e );
530         }
531     }
532 
533     private FileLogger setupLogger( File basedir )
534         throws IOException
535     {
536         FileLogger logger = null;
537 
538         if ( !noLog )
539         {
540             File outputLog = new File( basedir, "build.log" );
541 
542             if ( streamLogs )
543             {
544                 logger = new FileLogger( outputLog, getLog() );
545             }
546             else
547             {
548                 logger = new FileLogger( outputLog );
549             }
550 
551             getLog().debug( "build log initialized in: " + outputLog );
552 
553         }
554 
555         return logger;
556     }
557 
558     class IntegrationTestFailure
559         extends Exception
560     {
561         IntegrationTestFailure()
562         {
563             super();
564         }
565 
566         IntegrationTestFailure( String message )
567         {
568             super( message );
569         }
570 
571         IntegrationTestFailure( Throwable cause )
572         {
573             super( cause );
574         }
575 
576         IntegrationTestFailure( String message, Throwable cause )
577         {
578             super( message, cause );
579         }
580     }
581 
582     /**
583      * Returns the map-based value source used to interpolate settings and other stuff.
584      *
585      * @return The map-based value source for interpolation, never <code>null</code>.
586      */
587     private Map<String, Object> getInterpolationValueSource()
588     {
589         Map<String, Object> props = new HashMap<String, Object>();
590         if ( filterProperties != null )
591         {
592             props.putAll( filterProperties );
593         }
594         if ( filterProperties != null )
595         {
596             props.putAll( filterProperties );
597         }
598         props.put( "basedir", this.project.getBasedir().getAbsolutePath() );
599         props.put( "baseurl", toUrl( this.project.getBasedir().getAbsolutePath() ) );
600         if ( settings.getLocalRepository() != null )
601         {
602             props.put( "localRepository", settings.getLocalRepository() );
603             props.put( "localRepositoryUrl", toUrl( settings.getLocalRepository() ) );
604         }
605         return new CompositeMap( this.project, props );
606     }
607 
608     protected void buildInterpolatedFile( File originalFile, File interpolatedFile )
609         throws MojoExecutionException
610     {
611         getLog().debug( "Interpolate " + originalFile.getPath() + " to " + interpolatedFile.getPath() );
612 
613         try
614         {
615             String xml;
616 
617             Reader reader = null;
618             try
619             {
620                 // interpolation with token @...@
621                 Map<String, Object> composite = getInterpolationValueSource();
622                 reader = ReaderFactory.newXmlReader( originalFile );
623                 reader = new InterpolationFilterReader( reader, composite, "@", "@" );
624                 xml = IOUtil.toString( reader );
625             }
626             finally
627             {
628                 IOUtil.close( reader );
629             }
630 
631             Writer writer = null;
632             try
633             {
634                 interpolatedFile.getParentFile().mkdirs();
635                 writer = WriterFactory.newXmlWriter( interpolatedFile );
636                 writer.write( xml );
637                 writer.flush();
638             }
639             finally
640             {
641                 IOUtil.close( writer );
642             }
643         }
644         catch ( IOException e )
645         {
646             throw new MojoExecutionException( "Failed to interpolate file " + originalFile.getPath(), e );
647         }
648     }
649 
650     private static class CompositeMap
651         implements Map<String, Object>
652     {
653 
654         /**
655          * The Maven project from which to extract interpolated values, never <code>null</code>.
656          */
657         private MavenProject mavenProject;
658 
659         /**
660          * The set of additional properties from which to extract interpolated values, never <code>null</code>.
661          */
662         private Map<String, Object> properties;
663 
664         /**
665          * Creates a new interpolation source backed by the specified Maven project and some user-specified properties.
666          *
667          * @param mavenProject The Maven project from which to extract interpolated values, must not be <code>null</code>.
668          * @param properties   The set of additional properties from which to extract interpolated values, may be
669          *                     <code>null</code>.
670          */
671         protected CompositeMap( MavenProject mavenProject, Map<String, Object> properties )
672         {
673             if ( mavenProject == null )
674             {
675                 throw new IllegalArgumentException( "no project specified" );
676             }
677             this.mavenProject = mavenProject;
678             this.properties = properties == null ? (Map) new Properties() : properties;
679         }
680 
681         /**
682          * {@inheritDoc}
683          *
684          * @see java.util.Map#clear()
685          */
686         public void clear()
687         {
688             // nothing here
689         }
690 
691         /**
692          * {@inheritDoc}
693          *
694          * @see java.util.Map#containsKey(java.lang.Object)
695          */
696         public boolean containsKey( Object key )
697         {
698             if ( !( key instanceof String ) )
699             {
700                 return false;
701             }
702 
703             String expression = (String) key;
704             if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
705             {
706                 try
707                 {
708                     Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
709                     if ( evaluated != null )
710                     {
711                         return true;
712                     }
713                 }
714                 catch ( Exception e )
715                 {
716                     // uhm do we have to throw a RuntimeException here ?
717                 }
718             }
719 
720             return properties.containsKey( key ) || mavenProject.getProperties().containsKey( key );
721         }
722 
723         /**
724          * {@inheritDoc}
725          *
726          * @see java.util.Map#containsValue(java.lang.Object)
727          */
728         public boolean containsValue( Object value )
729         {
730             throw new UnsupportedOperationException();
731         }
732 
733         /**
734          * {@inheritDoc}
735          *
736          * @see java.util.Map#entrySet()
737          */
738         public Set<Entry<String, Object>> entrySet()
739         {
740             throw new UnsupportedOperationException();
741         }
742 
743         /**
744          * {@inheritDoc}
745          *
746          * @see java.util.Map#get(java.lang.Object)
747          */
748         public Object get( Object key )
749         {
750             if ( !( key instanceof String ) )
751             {
752                 return null;
753             }
754 
755             String expression = (String) key;
756             if ( expression.startsWith( "project." ) || expression.startsWith( "pom." ) )
757             {
758                 try
759                 {
760                     Object evaluated = ReflectionValueExtractor.evaluate( expression, this.mavenProject );
761                     if ( evaluated != null )
762                     {
763                         return evaluated;
764                     }
765                 }
766                 catch ( Exception e )
767                 {
768                     // uhm do we have to throw a RuntimeException here ?
769                 }
770             }
771 
772             Object value = properties.get( key );
773 
774             return ( value != null ? value : this.mavenProject.getProperties().get( key ) );
775 
776         }
777 
778         /**
779          * {@inheritDoc}
780          *
781          * @see java.util.Map#isEmpty()
782          */
783         public boolean isEmpty()
784         {
785             return this.mavenProject == null && this.mavenProject.getProperties().isEmpty()
786                 && this.properties.isEmpty();
787         }
788 
789         /**
790          * {@inheritDoc}
791          *
792          * @see java.util.Map#keySet()
793          */
794         public Set<String> keySet()
795         {
796             throw new UnsupportedOperationException();
797         }
798 
799         /**
800          * {@inheritDoc}
801          *
802          * @see java.util.Map#put(java.lang.Object, java.lang.Object)
803          */
804         public Object put( String key, Object value )
805         {
806             throw new UnsupportedOperationException();
807         }
808 
809         /**
810          * {@inheritDoc}
811          *
812          * @see java.util.Map#putAll(java.util.Map)
813          */
814         public void putAll( Map<? extends String, ? extends Object> t )
815         {
816             throw new UnsupportedOperationException();
817         }
818 
819         /**
820          * {@inheritDoc}
821          *
822          * @see java.util.Map#remove(java.lang.Object)
823          */
824         public Object remove( Object key )
825         {
826             throw new UnsupportedOperationException();
827         }
828 
829         /**
830          * {@inheritDoc}
831          *
832          * @see java.util.Map#size()
833          */
834         public int size()
835         {
836             throw new UnsupportedOperationException();
837         }
838 
839         /**
840          * {@inheritDoc}
841          *
842          * @see java.util.Map#values()
843          */
844         public Collection<Object> values()
845         {
846             throw new UnsupportedOperationException();
847         }
848     }
849 
850 
851     /**
852      * Converts the specified filesystem path to a URL. The resulting URL has no trailing slash regardless whether the
853      * path denotes a file or a directory.
854      *
855      * @param filename The filesystem path to convert, must not be <code>null</code>.
856      * @return The <code>file:</code> URL for the specified path, never <code>null</code>.
857      */
858     private static String toUrl( String filename )
859     {
860         /*
861          * NOTE: Maven fails to properly handle percent-encoded "file:" URLs (WAGON-111) so don't use File.toURI() here
862          * as-is but use the decoded path component in the URL.
863          */
864         String url = "file://" + new File( filename ).toURI().getPath();
865         if ( url.endsWith( "/" ) )
866         {
867             url = url.substring( 0, url.length() - 1 );
868         }
869         return url;
870     }
871 }