Coverage Report - org.apache.maven.plugin.patch.ApplyMojo
 
Classes in this File Line Coverage Branch Coverage Complexity
ApplyMojo
0%
0/150
0%
0/80
7,111
ApplyMojo$1
0%
0/5
0%
0/2
7,111
 
 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  0
 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  0
         List watches = new ArrayList();
 64  
 
 65  0
         watches.add( "fail" );
 66  0
         watches.add( "skip" );
 67  0
         watches.add( "reject" );
 68  
 
 69  0
         PATCH_FAILURE_WATCH_PHRASES = watches;
 70  
 
 71  0
         List ignored = new ArrayList();
 72  
 
 73  0
         ignored.add( ".svn" );
 74  0
         ignored.add( "CVS" );
 75  
 
 76  0
         DEFAULT_IGNORED_PATCHES = ignored;
 77  
 
 78  0
         List ignoredPatterns = new ArrayList();
 79  
 
 80  0
         ignoredPatterns.add( ".svn/**" );
 81  0
         ignoredPatterns.add( "CVS/**" );
 82  
 
 83  0
         DEFAULT_IGNORED_PATCH_PATTERNS = ignoredPatterns;
 84  0
     }
 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  0
     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  0
         boolean patchDirEnabled = ( ( patches != null ) && !patches.isEmpty() ) || naturalOrderProcessing;
 259  0
         boolean patchFileEnabled = patchFile != null;
 260  
 
 261  
         // if patches is null or empty, and naturalOrderProcessing is not true then disable patching
 262  0
         if ( !patchFileEnabled && !patchDirEnabled )
 263  
         {
 264  0
             getLog().info( "Patching is disabled for this project." );
 265  0
             return;
 266  
         }
 267  
 
 268  0
         if ( skipApplication )
 269  
         {
 270  0
             getLog().info( "Skipping patch file application (per configuration)." );
 271  0
             return;
 272  
         }
 273  
 
 274  0
         patchTrackingFile.getParentFile().mkdirs();
 275  
 
 276  0
         Map patchesToApply = null;
 277  
 
 278  
         try
 279  
         {
 280  0
             if ( patchFileEnabled )
 281  
             {
 282  0
                 patchesToApply = Collections.singletonMap( patchFile.getName(), createPatchCommand( patchFile ) );
 283  
             }
 284  
             else
 285  
             {
 286  0
                 if ( !patchDirectory.isDirectory() )
 287  
                 {
 288  0
                     throw new FileNotFoundException( "The base directory for patch files does not exist: "
 289  
                         + patchDirectory );
 290  
                 }
 291  
 
 292  0
                 List foundPatchFiles = FileUtils.getFileNames( patchDirectory, "*", null, false );
 293  
 
 294  0
                 patchesToApply = findPatchesToApply( foundPatchFiles, patchDirectory );
 295  
 
 296  0
                 checkStrictPatchCompliance( foundPatchFiles );
 297  
             }
 298  
 
 299  0
             String output = applyPatches( patchesToApply );
 300  
 
 301  0
             checkForWatchPhrases( output );
 302  
 
 303  0
             writeTrackingFile( patchesToApply );
 304  
         }
 305  0
         catch ( IOException ioe )
 306  
         {
 307  0
             throw new MojoExecutionException( "Unable to obtain list of patch files", ioe );
 308  0
         }
 309  0
     }
 310  
 
 311  
     private Map findPatchesToApply( List foundPatchFiles, File patchSourceDir )
 312  
         throws MojoFailureException
 313  
     {
 314  0
         Map patchesApplied = new LinkedHashMap();
 315  
 
 316  0
         if ( naturalOrderProcessing )
 317  
         {
 318  0
             patches = new ArrayList( foundPatchFiles );
 319  0
             Collections.sort( patches );
 320  
         }
 321  
 
 322  0
         String alreadyAppliedPatches = "";
 323  
 
 324  
         try
 325  
         {
 326  0
             if ( optimizations && patchTrackingFile.exists() )
 327  
             {
 328  0
                 alreadyAppliedPatches = FileUtils.fileRead( patchTrackingFile );
 329  
             }
 330  
         }
 331  0
         catch ( IOException ioe )
 332  
         {
 333  0
             throw new MojoFailureException( "unable to read patch tracking file: " + ioe.getMessage() );
 334  0
         }
 335  
 
 336  0
         for ( Iterator it = patches.iterator(); it.hasNext(); )
 337  
         {
 338  0
             String patch = (String) it.next();
 339  
 
 340  0
             if ( alreadyAppliedPatches.indexOf( patch ) == -1 )
 341  
             {
 342  0
                 File patchFile = new File( patchSourceDir, patch );
 343  
 
 344  0
                 getLog().debug( "Looking for patch: " + patch + " in: " + patchFile );
 345  
 
 346  0
                 if ( !patchFile.exists() )
 347  
                 {
 348  0
                     if ( strictPatching )
 349  
                     {
 350  0
                         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  0
                         getLog().info(
 359  
                                        "Skipping patch: " + patch + " listed in the parameter \"patches\"; "
 360  
                                            + "it is missing." );
 361  
                     }
 362  
                 }
 363  
                 else
 364  
                 {
 365  0
                     foundPatchFiles.remove( patch );
 366  
 
 367  0
                     patchesApplied.put( patch, createPatchCommand( patchFile ) );
 368  
                 }
 369  
             }
 370  
         }
 371  
 
 372  0
         return patchesApplied;
 373  
     }
 374  
 
 375  
     private void checkStrictPatchCompliance( List foundPatchFiles )
 376  
         throws MojoExecutionException
 377  
     {
 378  0
         if ( strictPatching )
 379  
         {
 380  0
             List ignored = new ArrayList();
 381  
 
 382  0
             if ( ignoredPatches != null )
 383  
             {
 384  0
                 ignored.addAll( ignoredPatches );
 385  
             }
 386  
 
 387  0
             if ( useDefaultIgnores )
 388  
             {
 389  0
                 ignored.addAll( DEFAULT_IGNORED_PATCHES );
 390  
             }
 391  
 
 392  0
             List limbo = new ArrayList( foundPatchFiles );
 393  
 
 394  0
             for ( Iterator it = ignored.iterator(); it.hasNext(); )
 395  
             {
 396  0
                 String ignoredFile = (String) it.next();
 397  
 
 398  0
                 limbo.remove( ignoredFile );
 399  
             }
 400  
 
 401  0
             if ( !limbo.isEmpty() )
 402  
             {
 403  0
                 StringBuffer extraFileBuffer = new StringBuffer();
 404  
 
 405  0
                 extraFileBuffer.append( "Found " + limbo.size() + " unlisted patch files:" );
 406  
 
 407  0
                 for ( Iterator it = foundPatchFiles.iterator(); it.hasNext(); )
 408  
                 {
 409  0
                     String patch = (String) it.next();
 410  
 
 411  0
                     extraFileBuffer.append( "\n  \'" ).append( patch ).append( '\'' );
 412  
                 }
 413  
 
 414  0
                 extraFileBuffer.append( "\n\nEither remove these files, "
 415  
                     + "add them to the patches configuration list, " + "or relax strictPatching." );
 416  
 
 417  0
                 throw new MojoExecutionException( extraFileBuffer.toString() );
 418  
             }
 419  
         }
 420  0
     }
 421  
 
 422  
     private String applyPatches( Map patchesApplied )
 423  
         throws MojoExecutionException
 424  
     {
 425  0
         final StringWriter outputWriter = new StringWriter();
 426  
 
 427  0
         StreamConsumer consumer = new StreamConsumer()
 428  
         {
 429  0
             public void consumeLine( String line )
 430  
             {
 431  0
                 if ( getLog().isDebugEnabled() )
 432  
                 {
 433  0
                     getLog().debug( line );
 434  
                 }
 435  
 
 436  0
                 outputWriter.write( line + "\n" );
 437  0
             }
 438  
         };
 439  
 
 440  
         // used if failFast is false
 441  0
         List failedPatches = new ArrayList();
 442  
 
 443  0
         for ( Iterator it = patchesApplied.entrySet().iterator(); it.hasNext(); )
 444  
         {
 445  0
             Map.Entry entry = (Entry) it.next();
 446  0
             String patchName = (String) entry.getKey();
 447  0
             Commandline cli = (Commandline) entry.getValue();
 448  
 
 449  
             try
 450  
             {
 451  0
                 getLog().info( "Applying patch: " + patchName );
 452  
 
 453  0
                 int result = executeCommandLine( cli, consumer, consumer );
 454  
 
 455  0
                 if ( result != 0 )
 456  
                 {
 457  0
                     if ( failFast )
 458  
                     {
 459  0
                         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  0
                         failedPatches.add( patchName );
 465  
                     }
 466  
                 }
 467  
             }
 468  0
             catch ( CommandLineException e )
 469  
             {
 470  0
                 throw new MojoExecutionException( "Failed to apply patch: " + patchName
 471  
                     + ". See debug output for more information.", e );
 472  0
             }
 473  
         }
 474  
 
 475  0
         if ( !failedPatches.isEmpty() )
 476  
         {
 477  0
             getLog().error( "Failed applying one or more patches:" );
 478  0
             for ( Iterator it = failedPatches.iterator(); it.hasNext(); )
 479  
             {
 480  0
                 getLog().error( "* " + it.next() );
 481  
             }
 482  0
             throw new MojoExecutionException( "Patch command failed for one or more patches."
 483  
                 + " Please see console and debug output for more information." );
 484  
         }
 485  
 
 486  0
         return outputWriter.toString();
 487  
     }
 488  
 
 489  
     private int executeCommandLine( Commandline cli, StreamConsumer out, StreamConsumer err )
 490  
         throws CommandLineException
 491  
     {
 492  0
         if ( getLog().isDebugEnabled() )
 493  
         {
 494  0
             getLog().debug( "Executing: " + cli );
 495  
         }
 496  
 
 497  0
         int result = CommandLineUtils.executeCommandLine( cli, out, err );
 498  
 
 499  0
         if ( getLog().isDebugEnabled() )
 500  
         {
 501  0
             getLog().debug( "Exit code: " + result );
 502  
         }
 503  
 
 504  0
         return result;
 505  
     }
 506  
 
 507  
     private void writeTrackingFile( Map patchesApplied )
 508  
         throws MojoExecutionException
 509  
     {
 510  0
         FileWriter writer = null;
 511  
         try
 512  
         {
 513  0
             boolean appending = patchTrackingFile.exists();
 514  
 
 515  0
             writer = new FileWriter( patchTrackingFile, appending );
 516  
 
 517  0
             for ( Iterator it = patchesApplied.keySet().iterator(); it.hasNext(); )
 518  
             {
 519  0
                 if ( appending )
 520  
                 {
 521  0
                     writer.write( System.getProperty( "line.separator" ) );
 522  
                 }
 523  
 
 524  0
                 String patch = (String) it.next();
 525  0
                 writer.write( patch );
 526  
 
 527  0
                 if ( it.hasNext() )
 528  
                 {
 529  0
                     writer.write( System.getProperty( "line.separator" ) );
 530  
                 }
 531  
             }
 532  
 
 533  0
             writer.flush();
 534  
         }
 535  0
         catch ( IOException e )
 536  
         {
 537  0
             throw new MojoExecutionException( "Failed to write patch-tracking file: " + patchTrackingFile, e );
 538  
         }
 539  
         finally
 540  
         {
 541  0
             IOUtil.close( writer );
 542  0
         }
 543  0
     }
 544  
 
 545  
     private void checkForWatchPhrases( String output )
 546  
         throws MojoExecutionException
 547  
     {
 548  0
         for ( Iterator it = failurePhrases.iterator(); it.hasNext(); )
 549  
         {
 550  0
             String phrase = (String) it.next();
 551  
 
 552  0
             if ( output.indexOf( phrase ) > -1 )
 553  
             {
 554  0
                 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  0
     }
 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  0
         Commandline cli = new Commandline();
 567  
 
 568  0
         cli.setExecutable( "patch" );
 569  
 
 570  0
         cli.setWorkingDirectory( targetDirectory.getAbsolutePath() );
 571  
 
 572  0
         cli.createArg().setValue( "-p" + strip );
 573  
 
 574  0
         if ( ignoreWhitespace )
 575  
         {
 576  0
             cli.createArg().setValue( "-l" );
 577  
         }
 578  
 
 579  0
         if ( reverse )
 580  
         {
 581  0
             cli.createArg().setValue( "-R" );
 582  
         }
 583  
 
 584  0
         if ( backups )
 585  
         {
 586  0
             cli.createArg().setValue( "-b" );
 587  
         }
 588  
 
 589  0
         if ( removeEmptyFiles )
 590  
         {
 591  0
             cli.createArg().setValue( "-E" );
 592  
         }
 593  
 
 594  0
         cli.createArg().setValue( "-i" );
 595  0
         cli.createArg().setFile( patchFile );
 596  
 
 597  0
         if ( destFile != null )
 598  
         {
 599  0
             cli.createArg().setValue( "-o" );
 600  0
             cli.createArg().setFile( destFile );
 601  
         }
 602  
 
 603  0
         if ( originalFile != null )
 604  
         {
 605  0
             cli.createArg().setFile( originalFile );
 606  
         }
 607  
 
 608  0
         return cli;
 609  
     }
 610  
 
 611  
 }