| package org.apache.maven.plugin.patch; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| import org.codehaus.plexus.util.FileUtils; |
| import org.codehaus.plexus.util.IOUtil; |
| import org.codehaus.plexus.util.cli.CommandLineException; |
| import org.codehaus.plexus.util.cli.CommandLineUtils; |
| import org.codehaus.plexus.util.cli.Commandline; |
| import org.codehaus.plexus.util.cli.StreamConsumer; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| /** |
| * Apply one or more patches to project sources. |
| * |
| * @goal apply |
| * @phase process-sources |
| */ |
| public class ApplyMojo |
| extends AbstractMojo |
| { |
| |
| public static final List PATCH_FAILURE_WATCH_PHRASES; |
| |
| public static final List DEFAULT_IGNORED_PATCHES; |
| |
| public static final List DEFAULT_IGNORED_PATCH_PATTERNS; |
| |
| static |
| { |
| List watches = new ArrayList(); |
| |
| watches.add( "fail" ); |
| watches.add( "skip" ); |
| watches.add( "reject" ); |
| |
| PATCH_FAILURE_WATCH_PHRASES = watches; |
| |
| List ignored = new ArrayList(); |
| |
| ignored.add( ".svn" ); |
| ignored.add( "CVS" ); |
| |
| DEFAULT_IGNORED_PATCHES = ignored; |
| |
| List ignoredPatterns = new ArrayList(); |
| |
| ignoredPatterns.add( ".svn/**" ); |
| ignoredPatterns.add( "CVS/**" ); |
| |
| DEFAULT_IGNORED_PATCH_PATTERNS = ignoredPatterns; |
| } |
| |
| /** |
| * Whether to exclude default ignored patch items, such as <code>.svn</code> or <code>CVS</code> directories. |
| * |
| * @parameter default-value="true" |
| */ |
| private boolean useDefaultIgnores; |
| |
| /** |
| * The list of patch file names, supplying the order in which patches should be applied. The path names in this list |
| * must be relative to the base directory specified by the parameter <code>patchDirectory</code>. This parameter |
| * is mutually exclusive with the <code>patchfile</code> parameter. |
| * |
| * @parameter |
| */ |
| protected List patches; |
| |
| /** |
| * Whether to skip this goal's execution. |
| * |
| * @parameter default-value="false" alias="patch.apply.skip" |
| */ |
| private boolean skipApplication; |
| |
| /** |
| * Flag to enable/disable optimization file from being written. This file tracks the patches that were applied the |
| * last time this goal actually executed. It is required for cases where project-sources optimizations are enabled, |
| * since project-sources will not be re-unpacked if they are at least as fresh as the source archive. If we avoid |
| * re-unpacking project sources, we need to make sure we don't reapply patches.<br/> <strong>Note:</strong> If the |
| * list of patches changes and this flag is enabled, a "<code>mvn clean</code>" must be executed before the next |
| * build, to remove the tracking file. |
| * |
| * @parameter default-value="true" |
| */ |
| private boolean optimizations; |
| |
| /** |
| * This is the tracking file used to maintain a list of the patches applied to the unpacked project sources which |
| * are currently in the target directory. If this file is present, and project-source unpacking is optimized |
| * (meaning it won't re-unpack unless the project-sources archive is newer), this goal will not execute and no |
| * patches will be applied in the current build. |
| * |
| * @parameter default-value="${project.build.directory}/optimization-files/patches-applied.txt" |
| */ |
| private File patchTrackingFile; |
| |
| /** |
| * The target directory for applying patches. Files in this directory will be modified. |
| * |
| * @parameter alias="patchTargetDir" default-value="${project.build.sourceDirectory}" |
| */ |
| private File targetDirectory; |
| |
| /** |
| * Flag being <code>true</code> if the desired behavior is to fail the build on the first failed patch detected. |
| * |
| * @parameter default-value="true" |
| */ |
| private boolean failFast; |
| |
| /** |
| * Setting natural order processing to <code>true</code> will cause all patches in a directory to be processed in |
| * a natural order alleviating the need to declare patches directly in the project file. |
| * |
| * @parameter default-value="false" |
| */ |
| private boolean naturalOrderProcessing; |
| |
| /** |
| * When the <code>strictPatching</code> flag is set, this parameter is useful to mark certain contents of the |
| * patch-source directory that should be ignored without causing the build to fail. |
| * |
| * @parameter |
| */ |
| private List ignoredPatches; |
| |
| /** |
| * Flag that, when set to <code>true</code>, will make sure that all patches included in the <code>patches</code> |
| * list must be present and describe the full contents of the patch directory. If <code>strictPatching</code> is |
| * set to <code>true</code>, and the <code>patches</code> list has a value that does not correspond to a file |
| * in the patch directory, the build will fail. If <code>strictPatching</code> is set to <code>true</code>, and |
| * the patch directory contains files not listed in the <code>patches</code> parameter, the build will fail. If |
| * set to <code>false</code>, only the patches listed in the <code>patches</code> parameter that have |
| * corresponding files will be applied; the rest will be ignored. |
| * |
| * @parameter default-value="false" |
| */ |
| private boolean strictPatching; |
| |
| /** |
| * The number of directories to be stripped from patch file paths, before applying, starting from the leftmost, or |
| * root-est. |
| * |
| * @parameter default-value="0" |
| */ |
| private int strip; |
| |
| /** |
| * Whether to ignore whitespaces when applying the patches. |
| * |
| * @parameter default-value="true" |
| */ |
| private boolean ignoreWhitespace; |
| |
| /** |
| * Whether to treat these patches as having reversed source and dest in the patch syntax. |
| * |
| * @parameter default-value="false" |
| */ |
| private boolean reverse; |
| |
| /** |
| * Whether to make backups of the original files before modding them. |
| * |
| * @parameter default-value="false" |
| */ |
| private boolean backups; |
| |
| /** |
| * List of phrases to watch for in the command output from the patch tool. If one is found, it will cause the build |
| * to fail. All phrases should be lower-case <em>only</em>. By default, the phrases <code>fail</code>, |
| * <code>skip</code> and <code>reject</code> are used. |
| * |
| * @parameter |
| */ |
| private List failurePhrases = PATCH_FAILURE_WATCH_PHRASES; |
| |
| /** |
| * The original file which will be modified by the patch. By default, the patch tool will automatically derive the |
| * original file from the header of the patch file. |
| * |
| * @parameter |
| */ |
| private File originalFile; |
| |
| /** |
| * The output file which is the original file, plus modifications from the patch. |
| * |
| * @parameter |
| */ |
| private File destFile; |
| |
| /** |
| * The single patch file to apply. This parameter is mutually exclusive with the <code>patches</code> parameter. |
| * |
| * @parameter |
| */ |
| private File patchFile; |
| |
| /** |
| * The base directory for the file names specified by the parameter <code>patches</code>. |
| * |
| * @parameter default-value="src/main/patches" |
| */ |
| private File patchDirectory; |
| |
| /** |
| * When set to <code>true</code>, the empty files resulting from the patching process are removed. Empty ancestor |
| * directories are removed as well. |
| * |
| * @parameter default-value="false" |
| * @since 1.1 |
| */ |
| private boolean removeEmptyFiles; |
| |
| /** |
| * Apply the patches. Give preference to patchFile over patchSourceDir/patches, and preference to originalFile over |
| * workDir. |
| */ |
| public void execute() |
| throws MojoExecutionException, MojoFailureException |
| { |
| boolean patchDirEnabled = ( ( patches != null ) && !patches.isEmpty() ) || naturalOrderProcessing; |
| boolean patchFileEnabled = patchFile != null; |
| |
| // if patches is null or empty, and naturalOrderProcessing is not true then disable patching |
| if ( !patchFileEnabled && !patchDirEnabled ) |
| { |
| getLog().info( "Patching is disabled for this project." ); |
| return; |
| } |
| |
| if ( skipApplication ) |
| { |
| getLog().info( "Skipping patch file application (per configuration)." ); |
| return; |
| } |
| |
| patchTrackingFile.getParentFile().mkdirs(); |
| |
| Map patchesToApply = null; |
| |
| try |
| { |
| if ( patchFileEnabled ) |
| { |
| patchesToApply = Collections.singletonMap( patchFile.getName(), createPatchCommand( patchFile ) ); |
| } |
| else |
| { |
| if ( !patchDirectory.isDirectory() ) |
| { |
| throw new FileNotFoundException( "The base directory for patch files does not exist: " |
| + patchDirectory ); |
| } |
| |
| List foundPatchFiles = FileUtils.getFileNames( patchDirectory, "*", null, false ); |
| |
| patchesToApply = findPatchesToApply( foundPatchFiles, patchDirectory ); |
| |
| checkStrictPatchCompliance( foundPatchFiles ); |
| } |
| |
| String output = applyPatches( patchesToApply ); |
| |
| checkForWatchPhrases( output ); |
| |
| writeTrackingFile( patchesToApply ); |
| } |
| catch ( IOException ioe ) |
| { |
| throw new MojoExecutionException( "Unable to obtain list of patch files", ioe ); |
| } |
| } |
| |
| private Map findPatchesToApply( List foundPatchFiles, File patchSourceDir ) |
| throws MojoFailureException |
| { |
| Map patchesApplied = new LinkedHashMap(); |
| |
| if ( naturalOrderProcessing ) |
| { |
| patches = new ArrayList( foundPatchFiles ); |
| Collections.sort( patches ); |
| } |
| |
| String alreadyAppliedPatches = ""; |
| |
| try |
| { |
| if ( optimizations && patchTrackingFile.exists() ) |
| { |
| alreadyAppliedPatches = FileUtils.fileRead( patchTrackingFile ); |
| } |
| } |
| catch ( IOException ioe ) |
| { |
| throw new MojoFailureException( "unable to read patch tracking file: " + ioe.getMessage() ); |
| } |
| |
| for ( Iterator it = patches.iterator(); it.hasNext(); ) |
| { |
| String patch = (String) it.next(); |
| |
| if ( alreadyAppliedPatches.indexOf( patch ) == -1 ) |
| { |
| File patchFile = new File( patchSourceDir, patch ); |
| |
| getLog().debug( "Looking for patch: " + patch + " in: " + patchFile ); |
| |
| if ( !patchFile.exists() ) |
| { |
| if ( strictPatching ) |
| { |
| throw new MojoFailureException( this, "Patch operation cannot proceed.", |
| "Cannot find specified patch: \'" + patch |
| + "\' in patch-source directory: \'" + patchSourceDir |
| + "\'.\n\nEither fix this error, " |
| + "or relax strictPatching." ); |
| } |
| else |
| { |
| getLog().info( |
| "Skipping patch: " + patch + " listed in the parameter \"patches\"; " |
| + "it is missing." ); |
| } |
| } |
| else |
| { |
| foundPatchFiles.remove( patch ); |
| |
| patchesApplied.put( patch, createPatchCommand( patchFile ) ); |
| } |
| } |
| } |
| |
| return patchesApplied; |
| } |
| |
| private void checkStrictPatchCompliance( List foundPatchFiles ) |
| throws MojoExecutionException |
| { |
| if ( strictPatching ) |
| { |
| List ignored = new ArrayList(); |
| |
| if ( ignoredPatches != null ) |
| { |
| ignored.addAll( ignoredPatches ); |
| } |
| |
| if ( useDefaultIgnores ) |
| { |
| ignored.addAll( DEFAULT_IGNORED_PATCHES ); |
| } |
| |
| List limbo = new ArrayList( foundPatchFiles ); |
| |
| for ( Iterator it = ignored.iterator(); it.hasNext(); ) |
| { |
| String ignoredFile = (String) it.next(); |
| |
| limbo.remove( ignoredFile ); |
| } |
| |
| if ( !limbo.isEmpty() ) |
| { |
| StringBuffer extraFileBuffer = new StringBuffer(); |
| |
| extraFileBuffer.append( "Found " + limbo.size() + " unlisted patch files:" ); |
| |
| for ( Iterator it = foundPatchFiles.iterator(); it.hasNext(); ) |
| { |
| String patch = (String) it.next(); |
| |
| extraFileBuffer.append( "\n \'" ).append( patch ).append( '\'' ); |
| } |
| |
| extraFileBuffer.append( "\n\nEither remove these files, " |
| + "add them to the patches configuration list, " + "or relax strictPatching." ); |
| |
| throw new MojoExecutionException( extraFileBuffer.toString() ); |
| } |
| } |
| } |
| |
| private String applyPatches( Map patchesApplied ) |
| throws MojoExecutionException |
| { |
| final StringWriter outputWriter = new StringWriter(); |
| |
| StreamConsumer consumer = new StreamConsumer() |
| { |
| public void consumeLine( String line ) |
| { |
| if ( getLog().isDebugEnabled() ) |
| { |
| getLog().debug( line ); |
| } |
| |
| outputWriter.write( line + "\n" ); |
| } |
| }; |
| |
| // used if failFast is false |
| String failedPatches = null; |
| |
| for ( Iterator it = patchesApplied.entrySet().iterator(); it.hasNext(); ) |
| { |
| Map.Entry entry = (Entry) it.next(); |
| String patchName = (String) entry.getKey(); |
| Commandline cli = (Commandline) entry.getValue(); |
| |
| try |
| { |
| getLog().info( "Applying patch: " + patchName ); |
| int result = executeCommandLine( cli, consumer, consumer ); |
| |
| getLog().info( "patch command returned: " + result ); |
| |
| if ( result != 0 ) |
| { |
| if ( failFast ) |
| { |
| throw new MojoExecutionException( "Patch command failed (exit value != 0) for " + patchName |
| + ". Please see debug output for more information." ); |
| } |
| else |
| { |
| if ( failedPatches == null ) |
| { |
| failedPatches = new String(); |
| } |
| failedPatches = failedPatches + patchName + "\n"; |
| } |
| } |
| } |
| catch ( CommandLineException e ) |
| { |
| throw new MojoExecutionException( "Failed to apply patch: " + patchName |
| + ". See debug output for more information.", e ); |
| } |
| } |
| |
| if ( failedPatches != null ) |
| { |
| getLog().info( "Failed applying one or more patches:" ); |
| getLog().info( failedPatches ); |
| throw new MojoExecutionException( "Patch command failed for one or more patches." |
| + " Please see console and debug output for more information." ); |
| } |
| |
| return outputWriter.toString(); |
| } |
| |
| private int executeCommandLine( Commandline cli, StreamConsumer out, StreamConsumer err ) |
| throws CommandLineException |
| { |
| if ( getLog().isDebugEnabled() ) |
| { |
| getLog().debug( "Executing:\n" + cli + "\n" ); |
| } |
| |
| getLog().info( Commandline.toString( cli.getShellCommandline() ) ); |
| |
| return CommandLineUtils.executeCommandLine( cli, out, err ); |
| } |
| |
| private void writeTrackingFile( Map patchesApplied ) |
| throws MojoExecutionException |
| { |
| FileWriter writer = null; |
| try |
| { |
| boolean appending = patchTrackingFile.exists(); |
| |
| writer = new FileWriter( patchTrackingFile, appending ); |
| |
| for ( Iterator it = patchesApplied.keySet().iterator(); it.hasNext(); ) |
| { |
| if ( appending ) |
| { |
| writer.write( System.getProperty( "line.separator" ) ); |
| } |
| |
| String patch = (String) it.next(); |
| writer.write( patch ); |
| |
| if ( it.hasNext() ) |
| { |
| writer.write( System.getProperty( "line.separator" ) ); |
| } |
| } |
| |
| writer.flush(); |
| } |
| catch ( IOException e ) |
| { |
| throw new MojoExecutionException( "Failed to write patch-tracking file: " + patchTrackingFile, e ); |
| } |
| finally |
| { |
| IOUtil.close( writer ); |
| } |
| } |
| |
| private void checkForWatchPhrases( String output ) |
| throws MojoExecutionException |
| { |
| for ( Iterator it = failurePhrases.iterator(); it.hasNext(); ) |
| { |
| String phrase = (String) it.next(); |
| |
| if ( output.indexOf( phrase ) > -1 ) |
| { |
| throw new MojoExecutionException( "Failed to apply patches (detected watch-phrase: \'" + phrase |
| + "\' in output). " + "If this is in error, configure the patchFailureWatchPhrases parameter." ); |
| } |
| } |
| } |
| |
| /** |
| * Add a new Patch task to the Ant calling mechanism. Give preference to originalFile/destFile, then workDir, and |
| * finally ${basedir}. |
| */ |
| private Commandline createPatchCommand( File patchFile ) |
| { |
| Commandline cli = new Commandline(); |
| |
| cli.setExecutable( "patch" ); |
| |
| cli.setWorkingDirectory( targetDirectory.getAbsolutePath() ); |
| |
| if ( originalFile != null ) |
| { |
| cli.createArg().setFile( originalFile ); |
| |
| if ( destFile != null ) |
| { |
| cli.createArg().setValue( "-o" ); |
| cli.createArg().setFile( destFile ); |
| } |
| |
| cli.createArg().setFile( patchFile ); |
| } |
| |
| cli.createArg().setValue( "-p" + strip ); |
| |
| if ( ignoreWhitespace ) |
| { |
| cli.createArg().setValue( "-l" ); |
| } |
| |
| if ( reverse ) |
| { |
| cli.createArg().setValue( "-R" ); |
| } |
| |
| if ( backups ) |
| { |
| cli.createArg().setValue( "-b" ); |
| } |
| |
| if ( removeEmptyFiles ) |
| { |
| cli.createArg().setValue( "-E" ); |
| } |
| |
| cli.createArg().setValue( "-i" ); |
| cli.createArg().setFile( patchFile ); |
| |
| return cli; |
| } |
| |
| } |