View Javadoc
1   package org.apache.maven.plugin.patch;
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.maven.plugin.AbstractMojo;
23  import org.apache.maven.plugin.MojoExecutionException;
24  import org.apache.maven.plugin.MojoFailureException;
25  import org.apache.maven.plugins.annotations.LifecyclePhase;
26  import org.apache.maven.plugins.annotations.Mojo;
27  import org.apache.maven.plugins.annotations.Parameter;
28  import org.codehaus.plexus.util.FileUtils;
29  import org.codehaus.plexus.util.IOUtil;
30  import org.codehaus.plexus.util.cli.CommandLineException;
31  import org.codehaus.plexus.util.cli.CommandLineUtils;
32  import org.codehaus.plexus.util.cli.Commandline;
33  import org.codehaus.plexus.util.cli.StreamConsumer;
34  
35  import java.io.File;
36  import java.io.FileNotFoundException;
37  import java.io.FileWriter;
38  import java.io.IOException;
39  import java.io.StringWriter;
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.Iterator;
43  import java.util.LinkedHashMap;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Map.Entry;
47  
48  /**
49   * Apply one or more patches to project sources.
50   */
51  @Mojo( name = "apply", defaultPhase = LifecyclePhase.PROCESS_SOURCES )
52  public class ApplyMojo
53      extends AbstractMojo
54  {
55  
56      public static final List PATCH_FAILURE_WATCH_PHRASES;
57  
58      public static final List DEFAULT_IGNORED_PATCHES;
59  
60      public static final List DEFAULT_IGNORED_PATCH_PATTERNS;
61  
62      static
63      {
64          List watches = new ArrayList();
65  
66          watches.add( "fail" );
67          watches.add( "skip" );
68          watches.add( "reject" );
69  
70          PATCH_FAILURE_WATCH_PHRASES = watches;
71  
72          List ignored = new ArrayList();
73  
74          ignored.add( ".svn" );
75          ignored.add( "CVS" );
76  
77          DEFAULT_IGNORED_PATCHES = ignored;
78  
79          List ignoredPatterns = new ArrayList();
80  
81          ignoredPatterns.add( ".svn/**" );
82          ignoredPatterns.add( "CVS/**" );
83  
84          DEFAULT_IGNORED_PATCH_PATTERNS = ignoredPatterns;
85      }
86  
87      /**
88       * Whether to exclude default ignored patch items, such as <code>.svn</code> or <code>CVS</code> directories.
89       */
90      @Parameter( defaultValue = "true" )
91      private boolean useDefaultIgnores;
92  
93      /**
94       * The list of patch file names, supplying the order in which patches should be applied. The path names in this list
95       * must be relative to the base directory specified by the parameter <code>patchDirectory</code>. This parameter
96       * is mutually exclusive with the <code>patchfile</code> parameter.
97       */
98      @Parameter
99      protected List patches;
100 
101     /**
102      * Whether to skip this goal's execution.
103      */
104     @Parameter( alias = "patch.apply.skip", defaultValue = "false" )
105     private boolean skipApplication;
106 
107     /**
108      * Flag to enable/disable optimization file from being written. This file tracks the patches that were applied the
109      * last time this goal actually executed. It is required for cases where project-sources optimizations are enabled,
110      * since project-sources will not be re-unpacked if they are at least as fresh as the source archive. If we avoid
111      * re-unpacking project sources, we need to make sure we don't reapply patches.<br/> <strong>Note:</strong> If the
112      * list of patches changes and this flag is enabled, a "<code>mvn clean</code>" must be executed before the next
113      * build, to remove the tracking file.
114      */
115     @Parameter( defaultValue = "true" )
116     private boolean optimizations;
117 
118     /**
119      * This is the tracking file used to maintain a list of the patches applied to the unpacked project sources which
120      * are currently in the target directory. If this file is present, and project-source unpacking is optimized
121      * (meaning it won't re-unpack unless the project-sources archive is newer), this goal will not execute and no
122      * patches will be applied in the current build.
123      */
124     @Parameter( defaultValue = "${project.build.directory}/optimization-files/patches-applied.txt" )
125     private File patchTrackingFile;
126 
127     /**
128      * The target directory for applying patches. Files in this directory will be modified.
129      */
130     @Parameter( alias = "patchTargetDir", defaultValue = "${project.build.sourceDirectory}" )
131     private File targetDirectory;
132 
133     /**
134      * Flag being <code>true</code> if the desired behavior is to fail the build on the first failed patch detected.
135      */
136     @Parameter( defaultValue = "true" )
137     private boolean failFast;
138 
139     /**
140      * Setting natural order processing to <code>true</code> will cause all patches in a directory to be processed in
141      * a natural order alleviating the need to declare patches directly in the project file.
142      */
143     @Parameter( defaultValue = "false" )
144     private boolean naturalOrderProcessing;
145 
146     /**
147      * When the <code>strictPatching</code> flag is set, this parameter is useful to mark certain contents of the
148      * patch-source directory that should be ignored without causing the build to fail.
149      */
150     @Parameter
151     private List ignoredPatches;
152 
153     /**
154      * Flag that, when set to <code>true</code>, will make sure that all patches included in the <code>patches</code>
155      * list must be present and describe the full contents of the patch directory. If <code>strictPatching</code> is
156      * set to <code>true</code>, and the <code>patches</code> list has a value that does not correspond to a file
157      * in the patch directory, the build will fail. If <code>strictPatching</code> is set to <code>true</code>, and
158      * the patch directory contains files not listed in the <code>patches</code> parameter, the build will fail. If
159      * set to <code>false</code>, only the patches listed in the <code>patches</code> parameter that have
160      * corresponding files will be applied; the rest will be ignored.
161      */
162     @Parameter( defaultValue = "false" )
163     private boolean strictPatching;
164 
165     /**
166      * The number of directories to be stripped from patch file paths, before applying, starting from the leftmost, or
167      * root-est.
168      */
169     @Parameter( defaultValue = "0" )
170     private int strip;
171 
172     /**
173      * Whether to ignore whitespaces when applying the patches.
174      */
175     @Parameter( defaultValue = "true" )
176     private boolean ignoreWhitespace;
177 
178     /**
179      * Whether to treat these patches as having reversed source and dest in the patch syntax.
180      */
181     @Parameter( defaultValue = "false" )
182     private boolean reverse;
183 
184     /**
185      * Whether to make backups of the original files before modding them.
186      */
187     @Parameter( defaultValue = "false" )
188     private boolean backups;
189 
190     /**
191      * List of phrases to watch for in the command output from the patch tool. If one is found, it will cause the build
192      * to fail. All phrases should be lower-case <em>only</em>. By default, the phrases <code>fail</code>,
193      * <code>skip</code> and <code>reject</code> are used.
194      */
195     @Parameter
196     private List failurePhrases = PATCH_FAILURE_WATCH_PHRASES;
197 
198     /**
199      * The original file which will be modified by the patch. By default, the patch tool will automatically derive the
200      * original file from the header of the patch file.
201      */
202     @Parameter
203     private File originalFile;
204 
205     /**
206      * The output file which is the original file, plus modifications from the patch. By default, the file(s) will be
207      * patched inplace.
208      */
209     @Parameter
210     private File destFile;
211 
212     /**
213      * The single patch file to apply. This parameter is mutually exclusive with the <code>patches</code> parameter.
214      */
215     @Parameter
216     private File patchFile;
217 
218     /**
219      * The base directory for the file names specified by the parameter <code>patches</code>.
220      */
221     @Parameter( defaultValue = "src/main/patches" )
222     private File patchDirectory;
223 
224     /**
225      * When set to <code>true</code>, the empty files resulting from the patching process are removed. Empty ancestor
226      * directories are removed as well.
227      *
228      * @since 1.1
229      */
230     @Parameter( defaultValue = "false" )
231     private boolean removeEmptyFiles;
232 
233     /**
234      * apply --binary option to patch command line
235      * @since 1.2
236      */
237     @Parameter( defaultValue = "false" )
238     private boolean binary;
239 
240     /**
241      * Apply the patches. Give preference to patchFile over patchSourceDir/patches, and preference to originalFile over
242      * workDir.
243      */
244     public void execute()
245         throws MojoExecutionException, MojoFailureException
246     {
247         boolean patchDirEnabled = ( ( patches != null ) && !patches.isEmpty() ) || naturalOrderProcessing;
248         boolean patchFileEnabled = patchFile != null;
249 
250         // if patches is null or empty, and naturalOrderProcessing is not true then disable patching
251         if ( !patchFileEnabled && !patchDirEnabled )
252         {
253             getLog().info( "Patching is disabled for this project." );
254             return;
255         }
256 
257         if ( skipApplication )
258         {
259             getLog().info( "Skipping patch file application (per configuration)." );
260             return;
261         }
262 
263         patchTrackingFile.getParentFile().mkdirs();
264 
265         Map patchesToApply;
266 
267         try
268         {
269             if ( patchFileEnabled )
270             {
271                 patchesToApply = Collections.singletonMap( patchFile.getName(), createPatchCommand( patchFile ) );
272             }
273             else
274             {
275                 if ( !patchDirectory.isDirectory() )
276                 {
277                     throw new FileNotFoundException( "The base directory for patch files does not exist: "
278                         + patchDirectory );
279                 }
280 
281                 List foundPatchFiles = FileUtils.getFileNames( patchDirectory, "*", null, false );
282 
283                 patchesToApply = findPatchesToApply( foundPatchFiles, patchDirectory );
284 
285                 checkStrictPatchCompliance( foundPatchFiles );
286             }
287 
288             String output = applyPatches( patchesToApply );
289 
290             checkForWatchPhrases( output );
291 
292             writeTrackingFile( patchesToApply );
293         }
294         catch ( IOException ioe )
295         {
296             throw new MojoExecutionException( "Unable to obtain list of patch files", ioe );
297         }
298     }
299 
300     private Map findPatchesToApply( List foundPatchFiles, File patchSourceDir )
301         throws MojoFailureException
302     {
303         Map patchesApplied = new LinkedHashMap();
304 
305         if ( naturalOrderProcessing )
306         {
307             patches = new ArrayList( foundPatchFiles );
308             Collections.sort( patches );
309         }
310 
311         String alreadyAppliedPatches = "";
312 
313         try
314         {
315             if ( optimizations && patchTrackingFile.exists() )
316             {
317                 alreadyAppliedPatches = FileUtils.fileRead( patchTrackingFile );
318             }
319         }
320         catch ( IOException ioe )
321         {
322             throw new MojoFailureException( "unable to read patch tracking file: " + ioe.getMessage() );
323         }
324 
325         for ( Object patche : patches )
326         {
327             String patch = (String) patche;
328 
329             if ( !alreadyAppliedPatches.contains( patch ) )
330             {
331                 File patchFile = new File( patchSourceDir, patch );
332 
333                 getLog().debug( "Looking for patch: " + patch + " in: " + patchFile );
334 
335                 if ( !patchFile.exists() )
336                 {
337                     if ( strictPatching )
338                     {
339                         throw new MojoFailureException( this, "Patch operation cannot proceed.",
340                                                         "Cannot find specified patch: \'" + patch
341                                                             + "\' in patch-source directory: \'" + patchSourceDir
342                                                             + "\'.\n\nEither fix this error, "
343                                                             + "or relax strictPatching." );
344                     }
345                     else
346                     {
347                         getLog().info( "Skipping patch: " + patch + " listed in the parameter \"patches\"; "
348                                            + "it is missing." );
349                     }
350                 }
351                 else
352                 {
353                     foundPatchFiles.remove( patch );
354 
355                     patchesApplied.put( patch, createPatchCommand( patchFile ) );
356                 }
357             }
358         }
359 
360         return patchesApplied;
361     }
362 
363     private void checkStrictPatchCompliance( List foundPatchFiles )
364         throws MojoExecutionException
365     {
366         if ( strictPatching )
367         {
368             List ignored = new ArrayList();
369 
370             if ( ignoredPatches != null )
371             {
372                 ignored.addAll( ignoredPatches );
373             }
374 
375             if ( useDefaultIgnores )
376             {
377                 ignored.addAll( DEFAULT_IGNORED_PATCHES );
378             }
379 
380             List limbo = new ArrayList( foundPatchFiles );
381 
382             for ( Object anIgnored : ignored )
383             {
384                 String ignoredFile = (String) anIgnored;
385 
386                 limbo.remove( ignoredFile );
387             }
388 
389             if ( !limbo.isEmpty() )
390             {
391                 StringBuilder extraFileBuffer = new StringBuilder();
392 
393                 extraFileBuffer.append( "Found " ).append( limbo.size() ).append( " unlisted patch files:" );
394 
395                 for ( Object foundPatchFile : foundPatchFiles )
396                 {
397                     String patch = (String) foundPatchFile;
398 
399                     extraFileBuffer.append( "\n  \'" ).append( patch ).append( '\'' );
400                 }
401 
402                 extraFileBuffer.append( "\n\nEither remove these files, "
403                     + "add them to the patches configuration list, " + "or relax strictPatching." );
404 
405                 throw new MojoExecutionException( extraFileBuffer.toString() );
406             }
407         }
408     }
409 
410     private String applyPatches( Map patchesApplied )
411         throws MojoExecutionException
412     {
413         final StringWriter outputWriter = new StringWriter();
414 
415         StreamConsumer consumer = new StreamConsumer()
416         {
417             public void consumeLine( String line )
418             {
419                 if ( getLog().isDebugEnabled() )
420                 {
421                     getLog().debug( line );
422                 }
423 
424                 outputWriter.write( line + "\n" );
425             }
426         };
427 
428         // used if failFast is false
429         List failedPatches = new ArrayList();
430 
431         for ( Object o : patchesApplied.entrySet() )
432         {
433             Entry entry = (Entry) o;
434             String patchName = (String) entry.getKey();
435             Commandline cli = (Commandline) entry.getValue();
436 
437             try
438             {
439                 getLog().info( "Applying patch: " + patchName );
440 
441                 int result = executeCommandLine( cli, consumer, consumer );
442 
443                 if ( result != 0 )
444                 {
445                     if ( failFast )
446                     {
447                         throw new MojoExecutionException( "Patch command failed with exit code " + result + " for "
448                             + patchName + ". Please see console and debug output for more information." );
449                     }
450                     else
451                     {
452                         failedPatches.add( patchName );
453                     }
454                 }
455             }
456             catch ( CommandLineException e )
457             {
458                 throw new MojoExecutionException( "Failed to apply patch: " + patchName
459                     + ". See debug output for more information.", e );
460             }
461         }
462 
463         if ( !failedPatches.isEmpty() )
464         {
465             getLog().error( "Failed applying one or more patches:" );
466             for ( Object failedPatche : failedPatches )
467             {
468                 getLog().error( "* " + failedPatche );
469             }
470             throw new MojoExecutionException( "Patch command failed for one or more patches."
471                 + " Please see console and debug output for more information." );
472         }
473 
474         return outputWriter.toString();
475     }
476 
477     private int executeCommandLine( Commandline cli, StreamConsumer out, StreamConsumer err )
478         throws CommandLineException
479     {
480         if ( getLog().isDebugEnabled() )
481         {
482             getLog().debug( "Executing: " + cli );
483         }
484 
485         int result = CommandLineUtils.executeCommandLine( cli, out, err );
486 
487         if ( getLog().isDebugEnabled() )
488         {
489             getLog().debug( "Exit code: " + result );
490         }
491 
492         return result;
493     }
494 
495     private void writeTrackingFile( Map patchesApplied )
496         throws MojoExecutionException
497     {
498         FileWriter writer = null;
499         try
500         {
501             boolean appending = patchTrackingFile.exists();
502 
503             writer = new FileWriter( patchTrackingFile, appending );
504 
505             for ( Iterator it = patchesApplied.keySet().iterator(); it.hasNext(); )
506             {
507                 if ( appending )
508                 {
509                     writer.write( System.getProperty( "line.separator" ) );
510                 }
511 
512                 String patch = (String) it.next();
513                 writer.write( patch );
514 
515                 if ( it.hasNext() )
516                 {
517                     writer.write( System.getProperty( "line.separator" ) );
518                 }
519             }
520 
521             writer.flush();
522         }
523         catch ( IOException e )
524         {
525             throw new MojoExecutionException( "Failed to write patch-tracking file: " + patchTrackingFile, e );
526         }
527         finally
528         {
529             IOUtil.close( writer );
530         }
531     }
532 
533     private void checkForWatchPhrases( String output )
534         throws MojoExecutionException
535     {
536         for ( Object failurePhrase : failurePhrases )
537         {
538             String phrase = (String) failurePhrase;
539 
540             if ( output.contains( phrase ) )
541             {
542                 throw new MojoExecutionException( "Failed to apply patches (detected watch-phrase: \'" + phrase
543                     + "\' in output). " + "If this is in error, configure the patchFailureWatchPhrases parameter." );
544             }
545         }
546     }
547 
548     /**
549      * Add a new Patch task to the Ant calling mechanism. Give preference to originalFile/destFile, then workDir, and
550      * finally ${basedir}.
551      */
552     private Commandline createPatchCommand( File patchFile )
553     {
554         Commandline cli = new Commandline();
555 
556         cli.setExecutable( "patch" );
557 
558         cli.setWorkingDirectory( targetDirectory.getAbsolutePath() );
559 
560         cli.createArg().setValue( "-p" + strip );
561 
562         if ( binary )
563         {
564             cli.createArg().setValue( "--binary" );
565         }
566 
567         if ( ignoreWhitespace )
568         {
569             cli.createArg().setValue( "-l" );
570         }
571 
572         if ( reverse )
573         {
574             cli.createArg().setValue( "-R" );
575         }
576 
577         if ( backups )
578         {
579             cli.createArg().setValue( "-b" );
580         }
581 
582         if ( removeEmptyFiles )
583         {
584             cli.createArg().setValue( "-E" );
585         }
586 
587         cli.createArg().setValue( "-i" );
588         cli.createArg().setFile( patchFile );
589 
590         if ( destFile != null )
591         {
592             cli.createArg().setValue( "-o" );
593             cli.createArg().setFile( destFile );
594         }
595 
596         if ( originalFile != null )
597         {
598             cli.createArg().setFile( originalFile );
599         }
600 
601         return cli;
602     }
603 
604 }