diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 07fdc9d17c..3c6303733d 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -2345,7 +2345,14 @@ public final class FilePath implements Serializable { } /** - * Default bound for {@link #validateAntFileMask(String, int)}. + * Same as {@link #validateFileMask(String, int, boolean)} with caseSensitive set to true + */ + public String validateAntFileMask(final String fileMasks, final int bound) throws IOException, InterruptedException { + return validateAntFileMask(fileMasks, bound, true); + } + + /** + * Default bound for {@link #validateAntFileMask(String, int, boolean)}. * @since 1.592 */ public static int VALIDATE_ANT_FILE_MASK_BOUND = Integer.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); @@ -2363,7 +2370,7 @@ public final class FilePath implements Serializable { * @throws InterruptedException not only in case of a channel failure, but also if too many operations were performed without finding any matches * @since 1.484 */ - public String validateAntFileMask(final String fileMasks, final int bound) throws IOException, InterruptedException { + public String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException { return act(new MasterToSlaveFileCallable() { private static final long serialVersionUID = 1; public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { @@ -2374,15 +2381,21 @@ public final class FilePath implements Serializable { while(tokens.hasMoreTokens()) { final String fileMask = tokens.nextToken().trim(); - if(hasMatch(dir,fileMask)) + if(hasMatch(dir,fileMask,caseSensitive)) continue; // no error on this portion + + // JENKINS-5253 - if we can get some match in case insensitive mode + // and user requested case sensitive match, notify the user + if (caseSensitive && hasMatch(dir, fileMask, false)) { + return Messages.FilePath_validateAntFileMask_matchWithCaseInsensitive(fileMask); + } // in 1.172 we introduced an incompatible change to stop using ' ' as the separator // so see if we can match by using ' ' as the separator if(fileMask.contains(" ")) { boolean matched = true; for (String token : Util.tokenize(fileMask)) - matched &= hasMatch(dir,token); + matched &= hasMatch(dir,token,caseSensitive); if(matched) return Messages.FilePath_validateAntFileMask_whitespaceSeprator(); } @@ -2398,7 +2411,7 @@ public final class FilePath implements Serializable { if(idx==-1) break; f=f.substring(idx+1); - if(hasMatch(dir,f)) + if(hasMatch(dir,f,caseSensitive)) return Messages.FilePath_validateAntFileMask_doesntMatchAndSuggest(fileMask,f); } } @@ -2406,6 +2419,7 @@ public final class FilePath implements Serializable { {// check the (2) above next as this is more expensive. // Try prepending "**/" to see if that results in a match FileSet fs = Util.createFileSet(reading(dir),"**/"+fileMask); + fs.setCaseSensitive(caseSensitive); DirectoryScanner ds = fs.getDirectoryScanner(new Project()); if(ds.getIncludedFilesCount()!=0) { // try shorter name first so that the suggestion results in least amount of changes @@ -2425,7 +2439,7 @@ public final class FilePath implements Serializable { prefix+=f.substring(0,idx)+'/'; f=f.substring(idx+1); - if(hasMatch(dir,prefix+fileMask)) + if(hasMatch(dir,prefix+fileMask,caseSensitive)) return Messages.FilePath_validateAntFileMask_doesntMatchAndSuggest(fileMask, prefix+fileMask); } } @@ -2437,7 +2451,7 @@ public final class FilePath implements Serializable { String pattern = fileMask; while(true) { - if(hasMatch(dir,pattern)) { + if(hasMatch(dir,pattern,caseSensitive)) { // found a match if(previous==null) return Messages.FilePath_validateAntFileMask_portionMatchAndSuggest(fileMask,pattern); @@ -2463,7 +2477,7 @@ public final class FilePath implements Serializable { return null; // no error } - private boolean hasMatch(File dir, String pattern) throws InterruptedException { + private boolean hasMatch(File dir, String pattern, boolean bCaseSensitive) throws InterruptedException { class Cancel extends RuntimeException {} DirectoryScanner ds = bound == Integer.MAX_VALUE ? new DirectoryScanner() : new DirectoryScanner() { int ticks; @@ -2481,6 +2495,7 @@ public final class FilePath implements Serializable { }; ds.setBasedir(reading(dir)); ds.setIncludes(new String[] {pattern}); + ds.setCaseSensitive(bCaseSensitive); try { ds.scan(); } catch (Cancel c) { @@ -2507,18 +2522,32 @@ public final class FilePath implements Serializable { } /** - * Shortcut for {@link #validateFileMask(String)} in case the left-hand side can be null. + * Short for {@code validateFileMask(path, value, true)} */ public static FormValidation validateFileMask(@CheckForNull FilePath path, String value) throws IOException { + return FilePath.validateFileMask(path, value, true); + } + + /** + * Shortcut for {@link #validateFileMask(String,true,boolean)} as the left-hand side can be null. + */ + public static FormValidation validateFileMask(@CheckForNull FilePath path, String value, boolean caseSensitive) throws IOException { if(path==null) return FormValidation.ok(); - return path.validateFileMask(value); + return path.validateFileMask(value, true, caseSensitive); } /** - * Short for {@code validateFileMask(value,true)} + * Short for {@code validateFileMask(value, true, true)} */ public FormValidation validateFileMask(String value) throws IOException { - return validateFileMask(value,true); + return validateFileMask(value, true, true); + } + + /** + * Short for {@code validateFileMask(value, errorIfNotExist, true)} + */ + public FormValidation validateFileMask(String value, boolean errorIfNotExist) throws IOException { + return validateFileMask(value, errorIfNotExist, true); } /** @@ -2527,7 +2556,7 @@ public final class FilePath implements Serializable { * or admin permission if no such ancestor is found. * @since 1.294 */ - public FormValidation validateFileMask(String value, boolean errorIfNotExist) throws IOException { + public FormValidation validateFileMask(String value, boolean errorIfNotExist, boolean caseSensitive) throws IOException { checkPermissionForValidate(); value = fixEmpty(value); @@ -2538,7 +2567,7 @@ public final class FilePath implements Serializable { if(!exists()) // no workspace. can't check return FormValidation.ok(); - String msg = validateAntFileMask(value, VALIDATE_ANT_FILE_MASK_BOUND); + String msg = validateAntFileMask(value, VALIDATE_ANT_FILE_MASK_BOUND, caseSensitive); if(errorIfNotExist) return FormValidation.error(msg); else return FormValidation.warning(msg); } catch (InterruptedException e) { diff --git a/core/src/main/java/hudson/tasks/ArtifactArchiver.java b/core/src/main/java/hudson/tasks/ArtifactArchiver.java index 17ad02f7c5..297300b525 100644 --- a/core/src/main/java/hudson/tasks/ArtifactArchiver.java +++ b/core/src/main/java/hudson/tasks/ArtifactArchiver.java @@ -97,6 +97,12 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { */ @Nonnull private Boolean defaultExcludes = true; + + /** + * Indicate whether include and exclude patterns should be considered as case sensitive + */ + @Nonnull + private Boolean caseSensitive = true; @DataBoundConstructor public ArtifactArchiver(String artifacts) { this.artifacts = artifacts.trim(); @@ -136,6 +142,9 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { if (defaultExcludes == null){ defaultExcludes = true; } + if (caseSensitive == null) { + caseSensitive = true; + } return this; } @@ -188,6 +197,14 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { @DataBoundSetter public final void setDefaultExcludes(boolean defaultExcludes) { this.defaultExcludes = defaultExcludes; } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + @DataBoundSetter public final void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } private void listenerWarnOrError(TaskListener listener, String message) { if (allowEmptyArchive) { @@ -214,7 +231,7 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { try { String artifacts = build.getEnvironment(listener).expand(this.artifacts); - Map files = ws.act(new ListFiles(artifacts, excludes, defaultExcludes)); + Map files = ws.act(new ListFiles(artifacts, excludes, defaultExcludes, caseSensitive)); if (!files.isEmpty()) { build.pickArtifactManager().archive(ws, launcher, BuildListenerAdapter.wrap(listener), files); if (fingerprint) { @@ -228,7 +245,7 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { listenerWarnOrError(listener, Messages.ArtifactArchiver_NoMatchFound(artifacts)); String msg = null; try { - msg = ws.validateAntFileMask(artifacts, FilePath.VALIDATE_ANT_FILE_MASK_BOUND); + msg = ws.validateAntFileMask(artifacts, FilePath.VALIDATE_ANT_FILE_MASK_BOUND, caseSensitive); } catch (Exception e) { listenerWarnOrError(listener, e.getMessage()); } @@ -253,17 +270,21 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { private static final long serialVersionUID = 1; private final String includes, excludes; private final boolean defaultExcludes; + private final boolean caseSensitive; - ListFiles(String includes, String excludes, boolean defaultExcludes) { + ListFiles(String includes, String excludes, boolean defaultExcludes, boolean caseSensitive) { this.includes = includes; this.excludes = excludes; this.defaultExcludes = defaultExcludes; + this.caseSensitive = caseSensitive; } + @Override public Map invoke(File basedir, VirtualChannel channel) throws IOException, InterruptedException { Map r = new HashMap(); FileSet fileSet = Util.createFileSet(basedir, includes, excludes); fileSet.setDefaultexcludes(defaultExcludes); + fileSet.setCaseSensitive(caseSensitive); for (String f : fileSet.getDirectoryScanner().getIncludedFiles()) { f = f.replace(File.separatorChar, '/'); @@ -296,13 +317,19 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { } /** - * Performs on-the-fly validation on the file mask wildcard. + * Performs on-the-fly validation of the file mask wildcard, when the artifacts + * textbox or the caseSensitive checkbox are modified */ - public FormValidation doCheckArtifacts(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { + public FormValidation doCheckArtifacts(@AncestorInPath AbstractProject project, + @QueryParameter String value, + @QueryParameter(value = "caseSensitive") String caseSensitive) + throws IOException { if (project == null) { return FormValidation.ok(); } - return FilePath.validateFileMask(project.getSomeWorkspace(),value); + // defensive approach to remain case sensitive in doubtful situations + boolean bCaseSensitive = caseSensitive == null || !"false".equals(caseSensitive); + return FilePath.validateFileMask(project.getSomeWorkspace(), value, bCaseSensitive); } @Override diff --git a/core/src/main/java/hudson/util/FormFieldValidator.java b/core/src/main/java/hudson/util/FormFieldValidator.java index a1932c6dc6..2fc8874a96 100644 --- a/core/src/main/java/hudson/util/FormFieldValidator.java +++ b/core/src/main/java/hudson/util/FormFieldValidator.java @@ -350,7 +350,7 @@ public abstract class FormFieldValidator { * Checks the file mask (specified in the 'value' query parameter) against * the current workspace. * @since 1.90. - * @deprecated as of 1.294. Use {@link FilePath#validateFileMask(String, boolean)} + * @deprecated as of 1.294. Use {@link FilePath#validateFileMask(String, boolean, boolean)} */ @Deprecated public static class WorkspaceFileMask extends FormFieldValidator { diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 00360f9b23..85c7803bfb 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -28,6 +28,7 @@ FilePath.validateAntFileMask.doesntMatchAndSuggest=\ FilePath.validateAntFileMask.portionMatchAndSuggest=\u2018{0}\u2019 doesn\u2019t match anything, although \u2018{1}\u2019 exists FilePath.validateAntFileMask.portionMatchButPreviousNotMatchAndSuggest=\u2018{0}\u2019 doesn\u2019t match anything: \u2018{1}\u2019 exists but not \u2018{2}\u2019 FilePath.validateAntFileMask.doesntMatchAnything=\u2018{0}\u2019 doesn\u2019t match anything +FilePath.validateAntFileMask.matchWithCaseInsensitive=\u2018{0}\u2019 doesn\u2019t match anything because it is treated case sensitively. You can deactivate case sensitivity to get matches FilePath.validateAntFileMask.doesntMatchAnythingAndSuggest=\u2018{0}\u2019 doesn\u2019t match anything: even \u2018{1}\u2019 doesn\u2019t exist FilePath.validateRelativePath.wildcardNotAllowed=Wildcard is not allowed here diff --git a/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly b/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly index cff5a42723..abb03fe4b9 100644 --- a/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly +++ b/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.jelly @@ -40,8 +40,11 @@ THE SOFTWARE. - - - + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.properties b/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.properties index 9fb285ad65..62475e2116 100644 --- a/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.properties +++ b/core/src/main/resources/hudson/tasks/ArtifactArchiver/config.properties @@ -22,4 +22,5 @@ allowEmptyArchive=Do not fail build if archiving returns nothing onlyIfSuccessful=Archive artifacts only if build is successful -defaultExcludes=Use default excludes \ No newline at end of file +defaultExcludes=Use default excludes +caseSensitive=Treat include and exclude patterns as case sensitive \ No newline at end of file diff --git a/core/src/main/resources/hudson/tasks/ArtifactArchiver/help-caseSensitive.html b/core/src/main/resources/hudson/tasks/ArtifactArchiver/help-caseSensitive.html new file mode 100644 index 0000000000..4d8200d063 --- /dev/null +++ b/core/src/main/resources/hudson/tasks/ArtifactArchiver/help-caseSensitive.html @@ -0,0 +1,5 @@ +
+ Artifact archiver uses Ant org.apache.tools.ant.DirectoryScanner which by default is case sensitive. + For instance, if the job produces *.hpi files, pattern "**/*.HPI" will fail to find them.

+ This option can be used to disable case sensitivity. When it's unchecked, pattern "**/*.HPI" will match any *.hpi files, or pattern "**/cAsEsEnSiTiVe.jar" will match a file called caseSensitive.jar. +
\ No newline at end of file diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index ed6ec0e9a5..e7491ca274 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -63,6 +63,7 @@ import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; + import org.jvnet.hudson.test.Issue; import org.mockito.Mockito; import static org.mockito.Mockito.*; @@ -539,6 +540,25 @@ public class FilePathTest { // good } } + + @Issue("5253") + public void testValidateCaseSensitivity() throws Exception { + File tmp = Util.createTempDir(); + try { + FilePath d = new FilePath(channels.french, tmp.getPath()); + d.child("d1/d2/d3").mkdirs(); + d.child("d1/d2/d3/f.txt").touch(0); + d.child("d1/d2/d3/f.html").touch(0); + d.child("d1/d2/f.txt").touch(0); + + assertEquals(null, d.validateAntFileMask("**/d1/**/f.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, true)); + assertEquals(null, d.validateAntFileMask("**/d1/**/f.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, false)); + assertEquals(Messages.FilePath_validateAntFileMask_matchWithCaseInsensitive("**/D1/**/F.*"), d.validateAntFileMask("**/D1/**/F.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, true)); + assertEquals(null, d.validateAntFileMask("**/D1/**/F.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, false)); + } finally { + Util.deleteRecursive(tmp); + } + } @Issue("JENKINS-15418") @Test public void deleteLongPathOnWindows() throws Exception {