Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 36 additions & 85 deletions core/src/main/java/hudson/model/AbstractProject.java
Original file line number Diff line number Diff line change
Expand Up @@ -1913,47 +1913,19 @@ public FormValidation doCheckAssignedLabelString(@AncestorInPath AbstractProject

public FormValidation doCheckLabel(@AncestorInPath AbstractProject<?,?> project,
@QueryParameter String value) {
return validateLabelExpression(value, project);
return LabelExpression.validate(value, project);
}

/**
* Validate label expression string.
*
* @param project May be specified to perform project specific validation.
* @since 1.590
* @deprecated Use {@link LabelExpression#validate(String, Item)} instead.
*/
@Deprecated
public static @NonNull FormValidation validateLabelExpression(String value, @CheckForNull AbstractProject<?, ?> project) {
if (Util.fixEmpty(value)==null)
return FormValidation.ok(); // nothing typed yet
try {
Label.parseExpression(value);
} catch (ANTLRException e) {
return FormValidation.error(e,
Messages.AbstractProject_AssignedLabelString_InvalidBooleanExpression(e.getMessage()));
}
Jenkins j = Jenkins.get();
Label l = j.getLabel(value);
if (l.isEmpty()) {
for (LabelAtom a : l.listAtoms()) {
if (a.isEmpty()) {
LabelAtom nearest = LabelAtom.findNearest(a.getName());
return FormValidation.warning(Messages.AbstractProject_AssignedLabelString_NoMatch_DidYouMean(a.getName(),nearest.getDisplayName()));
}
}
return FormValidation.warning(Messages.AbstractProject_AssignedLabelString_NoMatch());
}
if (project != null) {
for (AbstractProject.LabelValidator v : j
.getExtensionList(AbstractProject.LabelValidator.class)) {
FormValidation result = v.check(project, l);
if (!FormValidation.Kind.OK.equals(result.kind)) {
return result;
}
}
}
return FormValidation.okWithMarkup(Messages.AbstractProject_LabelLink(
j.getRootUrl(), Util.escape(l.getName()), l.getUrl(), l.getNodes().size(), l.getClouds().size())
);
return LabelExpression.validate(value, project);
}

public FormValidation doCheckCustomWorkspace(@QueryParameter String customWorkspace){
Expand Down Expand Up @@ -1985,64 +1957,12 @@ public AutoCompletionCandidates doAutoCompleteAssignedLabelString(@QueryParamete
}

public AutoCompletionCandidates doAutoCompleteLabel(@QueryParameter String value) {
AutoCompletionCandidates c = new AutoCompletionCandidates();
Set<Label> labels = Jenkins.get().getLabels();
List<String> queries = new AutoCompleteSeeder(value).getSeeds();

for (String term : queries) {
for (Label l : labels) {
if (l.getName().startsWith(term)) {
c.add(l.getName());
}
}
}
return c;
return LabelExpression.autoComplete(value);
}

public List<SCMCheckoutStrategyDescriptor> getApplicableSCMCheckoutStrategyDescriptors(AbstractProject p) {
return SCMCheckoutStrategyDescriptor._for(p);
}

/**
* Utility class for taking the current input value and computing a list
* of potential terms to match against the list of defined labels.
*/
static class AutoCompleteSeeder {
private String source;

AutoCompleteSeeder(String source) {
this.source = source;
}

List<String> getSeeds() {
ArrayList<String> terms = new ArrayList<>();
boolean trailingQuote = source.endsWith("\"");
boolean leadingQuote = source.startsWith("\"");
boolean trailingSpace = source.endsWith(" ");

if (trailingQuote || (trailingSpace && !leadingQuote)) {
terms.add("");
} else {
if (leadingQuote) {
int quote = source.lastIndexOf('"');
if (quote == 0) {
terms.add(source.substring(1));
} else {
terms.add("");
}
} else {
int space = source.lastIndexOf(' ');
if (space > -1) {
terms.add(source.substring(space+1));
} else {
terms.add(source);
}
}
}

return terms;
}
}
}

/**
Expand Down Expand Up @@ -2128,17 +2048,48 @@ public void setCustomWorkspace(String customWorkspace) throws IOException {
* This extension point allows such restrictions.
*
* @since 1.540
* @deprecated Use {@link jenkins.model.labels.LabelValidator} instead.
*/
@Deprecated
public static abstract class LabelValidator implements ExtensionPoint {

/**
* Check the use of the label within the specified context.
* <p>
* Note that "OK" responses (and any text/markup that may be set on them) will be ignored. Only warnings and
* errors are taken into account, and aggregated across all validators.
*
* @param project the project that wants to restrict itself to the specified label.
* @param label the label that the project wants to restrict itself to.
* @return the {@link FormValidation} result.
*/
@NonNull
public abstract FormValidation check(@NonNull AbstractProject<?, ?> project, @NonNull Label label);

/**
* Validates the use of a label within a particular context.
* <p>
* Note that "OK" responses (and any text/markup that may be set on them) will be ignored. Only warnings and
* errors are taken into account, and aggregated across all validators.
* <p>
* This method exists to allow plugins to implement an override for it, enabling checking in non-AbstractProject
* contexts without needing to update their Jenkins dependency (and using the new
* {@link jenkins.model.labels.LabelValidator} instead).
*
* @param item The context item to be restricted by the label.
* @param label The label that the job wants to restrict itself to.
* @return The validation result.
*
* @since TODO
*/
@NonNull
public FormValidation checkItem(@NonNull Item item, @NonNull Label label) {
if (item instanceof AbstractProject<?, ?>) {
return this.check((AbstractProject<?, ?>) item, label);
}
return FormValidation.ok();
}

}

}
115 changes: 115 additions & 0 deletions core/src/main/java/hudson/model/labels/LabelExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,24 @@
*/
package hudson.model.labels;

import antlr.ANTLRException;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Util;
import hudson.model.AbstractProject;
import hudson.model.AutoCompletionCandidates;
import hudson.model.Item;
import hudson.model.Label;
import hudson.model.Messages;
import hudson.util.FormValidation;
import hudson.util.VariableResolver;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import jenkins.model.Jenkins;
import jenkins.model.labels.LabelAutoCompleteSeeder;
import jenkins.model.labels.LabelValidator;

/**
* Boolean expression of labels.
Expand Down Expand Up @@ -210,4 +226,103 @@ public LabelOperatorPrecedence precedence() {
return LabelOperatorPrecedence.IMPLIES;
}
}

//region Auto-Completion and Validation

/**
* Generates auto-completion candidates for a (partial) label.
*
* @param label The (partial) label for which auto-completion is being requested.
* @return A set of auto-completion candidates.
* @since TODO
*/
@NonNull
public static AutoCompletionCandidates autoComplete(@Nullable String label) {
AutoCompletionCandidates c = new AutoCompletionCandidates();
Set<Label> labels = Jenkins.get().getLabels();
List<String> queries = new LabelAutoCompleteSeeder(Util.fixNull(label)).getSeeds();
for (String term : queries) {
for (Label l : labels) {
if (l.getName().startsWith(term)) {
c.add(l.getName());
}
}
}
return c;
}

/**
* Validates a label expression.
*
* @param expression The expression to validate.
* @return The validation result.
* @since TODO
*/
@NonNull
public static FormValidation validate(@Nullable String expression) {
return LabelExpression.validate(expression, null);
}

/**
* Validates a label expression.
*
* @param expression The label expression to validate.
* @param item The context item (like a job or a folder), if applicable; used for potential additional
* restrictions via {@link LabelValidator} instances.
* @return The validation result.
* @since TODO
*/
// FIXME: Should the messages be moved, or kept where they are for backward compatibility?
@NonNull
public static FormValidation validate(@Nullable String expression, @CheckForNull Item item) {
if (Util.fixEmptyAndTrim(expression) == null) {
return FormValidation.ok();
}
try {
Label.parseExpression(expression);
} catch (ANTLRException e) {
return FormValidation.error(e, Messages.LabelExpression_InvalidBooleanExpression(e.getMessage()));
}
final Jenkins j = Jenkins.get();
Label l = j.getLabel(expression);
if (l.isEmpty()) {
for (LabelAtom a : l.listAtoms()) {
if (a.isEmpty()) {
LabelAtom nearest = LabelAtom.findNearest(a.getName());
return FormValidation.warning(Messages.LabelExpression_NoMatch_DidYouMean(a.getName(), nearest.getDisplayName()));
}
}
return FormValidation.warning(Messages.LabelExpression_NoMatch());
}
if (item != null) {
final List<FormValidation> problems = new ArrayList<>();
// Use the project-oriented validators too, so that validation from older plugins still gets applied.
for (AbstractProject.LabelValidator v : j.getExtensionList(AbstractProject.LabelValidator.class)) {
FormValidation result = v.checkItem(item, l);
if (FormValidation.Kind.OK.equals(result.kind)) {
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I think if

final StringBuilder sb = new StringBuilder("<ul style='list-style-type: none; padding-left: 0; margin: 0'>");
FormValidation.Kind worst = Kind.OK;
for (FormValidation validation: validations) {
sb.append("<li>").append(validation.renderHtml()).append("</li>");
if (validation.kind.ordinal() > worst.ordinal()) {
worst = validation.kind;
}
}
sb.append("</ul>");
return respond(worst, sb.toString());
were refined slightly, to ignore all occurrences of FormValidation.OK in its input, then the API and this impl could be simplified a bit while actually handling ok(String) from validators. Not necessary in this PR, just something I noticed while looking at aggregate.

Copy link
Contributor Author

@Zastai Zastai Jun 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I looked there too. But I'm not sure it would help to get bulleted list entries for "Label is valid" (assuming a validator might put that in the OK text) among the errors/warnings.

Copy link
Member

@jglick jglick Jun 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right—aggregate would need to be made smarter, perhaps:

  • if no arguments, or all FormValidation.OK, return FormValidation.OK
  • else consider all non-FormValidation.OK arguments with the worst status among the bunch, and
    • if only one, return that as is
    • else return a bulleted list of those messages

}
problems.add(result);
}
// And then use the new validators.
for (LabelValidator v : j.getExtensionList(LabelValidator.class)) {
FormValidation result = v.check(item, l);
if (FormValidation.Kind.OK.equals(result.kind)) {
continue;
}
problems.add(result);
}
// If there were any problems, report them all.
if (!problems.isEmpty()) {
return FormValidation.aggregate(problems);
}
}
// All done. Report the results.
return FormValidation.okWithMarkup(Messages.LabelExpression_LabelLink(
j.getRootUrl(), Util.escape(l.getName()), l.getUrl(), l.getNodes().size(), l.getClouds().size())
);
}

//endregion

}
14 changes: 14 additions & 0 deletions core/src/main/java/hudson/tools/ToolInstallerDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
package hudson.tools;

import hudson.DescriptorExtensionList;
import hudson.model.AutoCompletionCandidates;
import hudson.model.Descriptor;
import hudson.model.labels.LabelExpression;
import hudson.util.FormValidation;
import jenkins.model.Jenkins;

import java.util.List;
import java.util.ArrayList;
import org.kohsuke.stapler.QueryParameter;

/**
* Descriptor for a {@link ToolInstaller}.
Expand Down Expand Up @@ -62,4 +66,14 @@ public static List<ToolInstallerDescriptor<?>> for_(Class<? extends ToolInstalla
return r;
}

@SuppressWarnings("unused")
public AutoCompletionCandidates doAutoCompleteLabel(@QueryParameter String value) {
return LabelExpression.autoComplete(value);
}

@SuppressWarnings("unused")
public FormValidation doCheckLabel(@QueryParameter String value) {
return LabelExpression.validate(value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package jenkins.model.labels;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Utility class for taking the current input value and computing a list of potential terms to match against the
* list of defined labels.
*/
@Restricted(NoExternalUse.class)
public class LabelAutoCompleteSeeder {

private final String source;

/**
* Creates a new auto-complete seeder for labels.
*
* @param source The (partial) label expression to use as the source..
*/
public LabelAutoCompleteSeeder(@NonNull String source) {
this.source = source;
}

/**
* Gets a list of seeds for label auto-completion.
*
* @return A list of seeds for label auto-completion.
*/
@NonNull
public List<String> getSeeds() {
final ArrayList<String> terms = new ArrayList<>();
boolean trailingQuote = source.endsWith("\"");
boolean leadingQuote = source.startsWith("\"");
boolean trailingSpace = source.endsWith(" ");
if (trailingQuote || (trailingSpace && !leadingQuote)) {
terms.add("");
} else {
if (leadingQuote) {
int quote = source.lastIndexOf('"');
if (quote == 0) {
terms.add(source.substring(1));
} else {
terms.add("");
}
} else {
int space = source.lastIndexOf(' ');
if (space > -1) {
terms.add(source.substring(space+1));
} else {
terms.add(source);
}
}
}
return terms;
}

}
Loading