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