Skip to content

Commit

Permalink
[JENKINS-5253] Allow case insensitive patterns in Artifacts Archiving
Browse files Browse the repository at this point in the history
  • Loading branch information
dominiquebrice committed Feb 1, 2015
1 parent 144a6da commit 40382c5
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 25 deletions.
57 changes: 43 additions & 14 deletions core/src/main/java/hudson/FilePath.java
Expand Up @@ -2342,7 +2342,14 @@ public String validateAntFileMask(final String fileMasks) throws IOException, In
}

/**
* 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);
Expand All @@ -2360,7 +2367,7 @@ public String validateAntFileMask(final String fileMasks) throws IOException, In
* @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<String>() {
private static final long serialVersionUID = 1;
public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
Expand All @@ -2371,15 +2378,21 @@ public String invoke(File dir, VirtualChannel channel) throws IOException, Inter

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();
}
Expand All @@ -2395,14 +2408,15 @@ public String invoke(File dir, VirtualChannel channel) throws IOException, Inter
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);
}
}

{// 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
Expand All @@ -2422,7 +2436,7 @@ public String invoke(File dir, VirtualChannel channel) throws IOException, Inter

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);
}
}
Expand All @@ -2434,7 +2448,7 @@ public String invoke(File dir, VirtualChannel channel) throws IOException, Inter
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);
Expand All @@ -2460,7 +2474,7 @@ public String invoke(File dir, VirtualChannel channel) throws IOException, Inter
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;
Expand All @@ -2478,6 +2492,7 @@ class Cancel extends RuntimeException {}
};
ds.setBasedir(reading(dir));
ds.setIncludes(new String[] {pattern});
ds.setCaseSensitive(bCaseSensitive);
try {
ds.scan();
} catch (Cancel c) {
Expand All @@ -2504,18 +2519,32 @@ private int findSeparator(String pattern) {
}

/**
* 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);
}

/**
Expand All @@ -2524,7 +2553,7 @@ public FormValidation validateFileMask(String value) throws IOException {
* 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);
Expand All @@ -2535,7 +2564,7 @@ public FormValidation validateFileMask(String value, boolean errorIfNotExist) th
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) {
Expand Down
39 changes: 33 additions & 6 deletions core/src/main/java/hudson/tasks/ArtifactArchiver.java
Expand Up @@ -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();
Expand Down Expand Up @@ -135,6 +141,9 @@ public Object readResolve() {
if (defaultExcludes == null){
defaultExcludes = true;
}
if (caseSensitive == null) {
caseSensitive = true;
}
return this;
}

Expand Down Expand Up @@ -187,6 +196,14 @@ public boolean isDefaultExcludes() {
@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) {
Expand All @@ -213,7 +230,7 @@ public void perform(Run<?,?> build, FilePath ws, Launcher launcher, TaskListener
try {
String artifacts = build.getEnvironment(listener).expand(this.artifacts);

Map<String,String> files = ws.act(new ListFiles(artifacts, excludes, defaultExcludes));
Map<String,String> files = ws.act(new ListFiles(artifacts, excludes, defaultExcludes, caseSensitive));
if (!files.isEmpty()) {
build.pickArtifactManager().archive(ws, launcher, BuildListenerAdapter.wrap(listener), files);
if (fingerprint) {
Expand All @@ -227,7 +244,7 @@ public void perform(Run<?,?> build, FilePath ws, Launcher launcher, TaskListener
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());
}
Expand All @@ -252,17 +269,21 @@ private static final class ListFiles extends MasterToSlaveFileCallable<Map<Strin
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<String,String> invoke(File basedir, VirtualChannel channel) throws IOException, InterruptedException {
Map<String,String> r = new HashMap<String,String>();

FileSet fileSet = Util.createFileSet(basedir, includes, excludes);
fileSet.setDefaultexcludes(defaultExcludes);
fileSet.setCaseSensitive(caseSensitive);

for (String f : fileSet.getDirectoryScanner().getIncludedFiles()) {
f = f.replace(File.separatorChar, '/');
Expand Down Expand Up @@ -294,13 +315,19 @@ public String getDisplayName() {
}

/**
* 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
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/util/FormFieldValidator.java
Expand Up @@ -346,7 +346,7 @@ protected void check() throws IOException, ServletException {
* 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)}
*/
public static class WorkspaceFileMask extends FormFieldValidator {
private final boolean errorIfNotExist;
Expand Down
1 change: 1 addition & 0 deletions core/src/main/resources/hudson/Messages.properties
Expand Up @@ -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
Expand Down
Expand Up @@ -40,8 +40,11 @@ THE SOFTWARE.
<f:entry field="fingerprint">
<f:checkbox title="${%Fingerprint all archived artifacts}"/>
</f:entry>
<f:entry field="defaultExcludes" >
<f:checkbox title="${%defaultExcludes}" default="true"/>
</f:entry>
<f:entry field="defaultExcludes" >
<f:checkbox title="${%defaultExcludes}" default="true"/>
</f:entry>
<f:entry field="caseSensitive" >
<f:checkbox title="${%caseSensitive}" default="true"/>
</f:entry>
</f:advanced>
</j:jelly>
Expand Up @@ -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
defaultExcludes=Use default excludes
caseSensitive=Treat include and exclude patterns as case sensitive
@@ -0,0 +1,5 @@
<div>
Artifact archiver uses Ant <code>org.apache.tools.ant.DirectoryScanner</code> which by default is case sensitive.
For instance, if the job produces *.hpi files, pattern "**/*.HPI" will fail to find them.<BR/><BR/>
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.
</div>
20 changes: 20 additions & 0 deletions core/src/test/java/hudson/FilePathTest.java
Expand Up @@ -59,6 +59,7 @@
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.*;
Expand Down Expand Up @@ -488,6 +489,25 @@ private static void assertValidateAntFileMask(String expected, FilePath d, Strin
// 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 {
Expand Down

0 comments on commit 40382c5

Please sign in to comment.