1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.patch;
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 import java.io.File;
41 import java.io.FileNotFoundException;
42 import java.io.FileWriter;
43 import java.io.IOException;
44 import java.io.StringWriter;
45 import java.io.Writer;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.Iterator;
49 import java.util.LinkedHashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Map.Entry;
53
54 import org.apache.maven.plugin.AbstractMojo;
55 import org.apache.maven.plugin.MojoExecutionException;
56 import org.apache.maven.plugin.MojoFailureException;
57 import org.apache.maven.plugins.annotations.LifecyclePhase;
58 import org.apache.maven.plugins.annotations.Mojo;
59 import org.apache.maven.plugins.annotations.Parameter;
60 import org.codehaus.plexus.util.FileUtils;
61 import org.codehaus.plexus.util.StringUtils;
62 import org.codehaus.plexus.util.cli.CommandLineException;
63 import org.codehaus.plexus.util.cli.CommandLineUtils;
64 import org.codehaus.plexus.util.cli.Commandline;
65 import org.codehaus.plexus.util.cli.StreamConsumer;
66
67
68
69
70 @Mojo(name = "apply", defaultPhase = LifecyclePhase.PROCESS_SOURCES)
71 public class ApplyMojo extends AbstractMojo {
72
73 public static final List<String> PATCH_FAILURE_WATCH_PHRASES;
74
75 public static final List<String> DEFAULT_IGNORED_PATCHES;
76
77 public static final List<String> DEFAULT_IGNORED_PATCH_PATTERNS;
78
79 static {
80 List<String> watches = new ArrayList<>();
81
82 watches.add("fail");
83 watches.add("skip");
84 watches.add("reject");
85
86 PATCH_FAILURE_WATCH_PHRASES = watches;
87
88 List<String> ignored = new ArrayList<>();
89
90 ignored.add(".svn");
91 ignored.add("CVS");
92
93 DEFAULT_IGNORED_PATCHES = ignored;
94
95 List<String> ignoredPatterns = new ArrayList<>();
96
97 ignoredPatterns.add(".svn/**");
98 ignoredPatterns.add("CVS/**");
99
100 DEFAULT_IGNORED_PATCH_PATTERNS = ignoredPatterns;
101 }
102
103
104
105
106 @Parameter(defaultValue = "true")
107 private boolean useDefaultIgnores;
108
109
110
111
112
113
114 @Parameter
115 protected List<String> patches;
116
117
118
119
120 @Parameter(alias = "patch.apply.skip", defaultValue = "false")
121 private boolean skipApplication;
122
123
124
125
126
127
128
129
130
131 @Parameter(defaultValue = "true")
132 private boolean optimizations;
133
134
135
136
137
138
139
140 @Parameter(defaultValue = "${project.build.directory}/optimization-files/patches-applied.txt")
141 private File patchTrackingFile;
142
143
144
145
146 @Parameter(alias = "patchTargetDir", defaultValue = "${project.build.sourceDirectory}")
147 private File targetDirectory;
148
149
150
151
152 @Parameter(defaultValue = "true")
153 private boolean failFast;
154
155
156
157
158
159 @Parameter(defaultValue = "false")
160 private boolean naturalOrderProcessing;
161
162
163
164
165
166 @Parameter
167 private List ignoredPatches;
168
169
170
171
172
173
174
175
176
177
178 @Parameter(defaultValue = "false")
179 private boolean strictPatching;
180
181
182
183
184
185 @Parameter(defaultValue = "0")
186 private int strip;
187
188
189
190
191 @Parameter(defaultValue = "true")
192 private boolean ignoreWhitespace;
193
194
195
196
197 @Parameter(defaultValue = "false")
198 private boolean reverse;
199
200
201
202
203 @Parameter(defaultValue = "false")
204 private boolean backups;
205
206
207
208
209
210
211 @Parameter
212 private List<String> failurePhrases = PATCH_FAILURE_WATCH_PHRASES;
213
214
215
216
217
218 @Parameter
219 private File originalFile;
220
221
222
223
224
225 @Parameter
226 private File destFile;
227
228
229
230
231 @Parameter
232 private File patchFile;
233
234
235
236
237 @Parameter(defaultValue = "src/main/patches")
238 private File patchDirectory;
239
240
241
242
243
244
245
246 @Parameter(defaultValue = "false")
247 private boolean removeEmptyFiles;
248
249
250
251
252
253 @Parameter(defaultValue = "false")
254 private boolean binary;
255
256
257
258
259
260
261
262
263 @Parameter
264 private List<String> excludes;
265
266
267
268
269
270 public void execute() throws MojoExecutionException, MojoFailureException {
271 boolean patchDirEnabled = ((patches != null) && !patches.isEmpty()) || naturalOrderProcessing;
272 boolean patchFileEnabled = patchFile != null;
273
274
275 if (!patchFileEnabled && !patchDirEnabled) {
276 getLog().info("Patching is disabled for this project.");
277 return;
278 }
279
280 if (skipApplication) {
281 getLog().info("Skipping patch file application (per configuration).");
282 return;
283 }
284
285 patchTrackingFile.getParentFile().mkdirs();
286
287 Map patchesToApply;
288
289 try {
290 if (patchFileEnabled) {
291 patchesToApply = Collections.singletonMap(patchFile.getName(), createPatchCommand(patchFile));
292 } else {
293 if (!patchDirectory.isDirectory()) {
294 throw new FileNotFoundException(
295 "The base directory for patch files does not exist: " + patchDirectory);
296 }
297
298 String excludePatterns = null;
299 if (excludes != null) {
300 excludePatterns = StringUtils.join(excludes.iterator(), ",");
301 getLog().info("Exclude pattern: " + excludePatterns);
302 }
303
304 List<String> foundPatchFiles = FileUtils.getFileNames(patchDirectory, "*", excludePatterns, false);
305
306 patchesToApply = findPatchesToApply(foundPatchFiles, patchDirectory);
307
308 checkStrictPatchCompliance(foundPatchFiles);
309 }
310
311 String output = applyPatches(patchesToApply);
312
313 checkForWatchPhrases(output);
314
315 writeTrackingFile(patchesToApply);
316 } catch (IOException ioe) {
317 throw new MojoExecutionException("Unable to obtain list of patch files", ioe);
318 }
319 }
320
321 private Map<String, Commandline> findPatchesToApply(List foundPatchFiles, File patchSourceDir)
322 throws MojoFailureException {
323 Map<String, Commandline> patchesApplied = new LinkedHashMap<>();
324
325 if (naturalOrderProcessing) {
326 patches = new ArrayList(foundPatchFiles);
327 Collections.sort(patches);
328 }
329
330 String alreadyAppliedPatches = "";
331
332 try {
333 if (optimizations && patchTrackingFile.exists()) {
334 alreadyAppliedPatches = FileUtils.fileRead(patchTrackingFile);
335 }
336 } catch (IOException ioe) {
337 throw new MojoFailureException("unable to read patch tracking file: " + ioe.getMessage());
338 }
339
340 for (String patch : patches) {
341 if (!alreadyAppliedPatches.contains(patch)) {
342 File patchFile = new File(patchSourceDir, patch);
343 getLog().debug("Looking for patch: " + patch + " in: " + patchFile);
344
345 if (!patchFile.exists()) {
346 if (strictPatching) {
347 throw new MojoFailureException(
348 this,
349 "Patch operation cannot proceed.",
350 "Cannot find specified patch: \'" + patch
351 + "\' in patch-source directory: \'" + patchSourceDir
352 + "\'.\n\nEither fix this error, "
353 + "or relax strictPatching.");
354 } else {
355 getLog().info("Skipping patch: " + patch + " listed in the parameter \"patches\"; "
356 + "it is missing.");
357 }
358 } else {
359 foundPatchFiles.remove(patch);
360
361 patchesApplied.put(patch, createPatchCommand(patchFile));
362 }
363 }
364 }
365
366 return patchesApplied;
367 }
368
369 private void checkStrictPatchCompliance(List<String> foundPatchFiles) throws MojoExecutionException {
370 if (strictPatching) {
371 List<String> ignored = new ArrayList<>();
372
373 if (ignoredPatches != null) {
374 ignored.addAll(ignoredPatches);
375 }
376
377 if (useDefaultIgnores) {
378 ignored.addAll(DEFAULT_IGNORED_PATCHES);
379 }
380
381 List<String> limbo = new ArrayList(foundPatchFiles);
382 limbo.removeAll(ignored);
383
384 if (!limbo.isEmpty()) {
385 StringBuilder extraFileBuffer = new StringBuilder();
386
387 extraFileBuffer.append("Found ").append(limbo.size()).append(" unlisted patch files:");
388
389 for (String patch : foundPatchFiles) {
390 extraFileBuffer.append("\n \'").append(patch).append('\'');
391 }
392
393 extraFileBuffer.append("\n\nEither remove these files, "
394 + "add them to the patches configuration list, " + "or relax strictPatching.");
395
396 throw new MojoExecutionException(extraFileBuffer.toString());
397 }
398 }
399 }
400
401 private String applyPatches(Map<String, Commandline> patchesApplied) throws MojoExecutionException {
402 final StringWriter outputWriter = new StringWriter();
403
404 StreamConsumer consumer = new StreamConsumer() {
405 public void consumeLine(String line) {
406 if (getLog().isDebugEnabled()) {
407 getLog().debug(line);
408 }
409
410 outputWriter.write(line + "\n");
411 }
412 };
413
414
415 List<String> failedPatches = new ArrayList<>();
416
417 for (Entry<String, Commandline> entry : patchesApplied.entrySet()) {
418 String patchName = entry.getKey();
419 Commandline cli = entry.getValue();
420
421 try {
422 getLog().info("Applying patch: " + patchName);
423
424 int result = executeCommandLine(cli, consumer, consumer);
425
426 if (result != 0) {
427 if (failFast) {
428 throw new MojoExecutionException("Patch command failed with exit code " + result + " for "
429 + patchName + ". Please see console and debug output for more information.");
430 } else {
431 failedPatches.add(patchName);
432 }
433 }
434 } catch (CommandLineException e) {
435 throw new MojoExecutionException(
436 "Failed to apply patch: " + patchName + ". See debug output for more information.", e);
437 }
438 }
439
440 if (!failedPatches.isEmpty()) {
441 getLog().error("Failed applying one or more patches:");
442 for (String failedPatch : failedPatches) {
443 getLog().error("* " + failedPatch);
444 }
445 throw new MojoExecutionException("Patch command failed for one or more patches."
446 + " See console and debug output for more information.");
447 }
448
449 return outputWriter.toString();
450 }
451
452 private int executeCommandLine(Commandline cli, StreamConsumer out, StreamConsumer err)
453 throws CommandLineException {
454 if (getLog().isDebugEnabled()) {
455 getLog().debug("Executing: " + cli);
456 }
457
458 int result = CommandLineUtils.executeCommandLine(cli, out, err);
459
460 if (getLog().isDebugEnabled()) {
461 getLog().debug("Exit code: " + result);
462 }
463
464 return result;
465 }
466
467 private void writeTrackingFile(Map patchesApplied) throws MojoExecutionException {
468 boolean appending = patchTrackingFile.exists();
469
470 try (Writer writer = new FileWriter(patchTrackingFile, appending)) {
471 for (Iterator it = patchesApplied.keySet().iterator(); it.hasNext(); ) {
472 if (appending) {
473 writer.write(System.getProperty("line.separator"));
474 }
475
476 String patch = (String) it.next();
477 writer.write(patch);
478
479 if (it.hasNext()) {
480 writer.write(System.getProperty("line.separator"));
481 }
482 }
483 } catch (IOException e) {
484 throw new MojoExecutionException("Failed to write patch-tracking file: " + patchTrackingFile, e);
485 }
486 }
487
488 private void checkForWatchPhrases(String output) throws MojoExecutionException {
489 for (String phrase : failurePhrases) {
490 if (output.contains(phrase)) {
491 throw new MojoExecutionException("Failed to apply patches (detected watch-phrase: \'" + phrase
492 + "\' in output). " + "If this is in error, configure the patchFailureWatchPhrases parameter.");
493 }
494 }
495 }
496
497
498
499
500
501 private Commandline createPatchCommand(File patchFile) {
502 Commandline cli = new Commandline();
503
504 cli.setExecutable("patch");
505
506 cli.setWorkingDirectory(targetDirectory.getAbsolutePath());
507
508 cli.createArg().setValue("-p" + strip);
509
510 if (binary) {
511 cli.createArg().setValue("--binary");
512 }
513
514 if (ignoreWhitespace) {
515 cli.createArg().setValue("-l");
516 }
517
518 if (reverse) {
519 cli.createArg().setValue("-R");
520 }
521
522 if (backups) {
523 cli.createArg().setValue("-b");
524 }
525
526 if (removeEmptyFiles) {
527 cli.createArg().setValue("-E");
528 }
529
530 cli.createArg().setValue("-i");
531 cli.createArg().setFile(patchFile);
532
533 if (destFile != null) {
534 cli.createArg().setValue("-o");
535 cli.createArg().setFile(destFile);
536 }
537
538 if (originalFile != null) {
539 cli.createArg().setFile(originalFile);
540 }
541
542 return cli;
543 }
544 }