/*******************************************************************************
*
* Copyright (c) 2004-2010 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Tom Huybrechts
*
*
*******************************************************************************/
package hudson.model;
import hudson.Util;
import static hudson.Util.fixNull;
import hudson.model.labels.LabelAtom;
import hudson.model.labels.LabelExpression;
import hudson.model.labels.LabelExpressionLexer;
import hudson.model.labels.LabelExpressionParser;
import hudson.model.labels.LabelOperatorPrecedence;
import hudson.slaves.NodeProvisioner;
import hudson.slaves.Cloud;
import hudson.util.QuotedStringTokenizer;
import hudson.util.VariableResolver;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Collection;
import java.util.TreeSet;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import hudson.matrix.Axis;
import hudson.matrix.MatrixProject;
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
/**
* Group of {@link Node}s.
*
* @author Kohsuke Kawaguchi
* @see Hudson#getLabels()
* @see Hudson#getLabel(String)
*/
@ExportedBean
public abstract class Label extends Actionable implements Comparable<Label>, ModelObject {
/**
* Display name of this label.
*/
protected transient final String name;
private transient volatile Set<Node> nodes;
private transient volatile Set<Cloud> clouds;
@Exported
public transient final LoadStatistics loadStatistics;
public transient final NodeProvisioner nodeProvisioner;
public Label(String name) {
this.name = name;
// passing these causes an infinite loop - getTotalExecutors(),getBusyExecutors());
this.loadStatistics = new LoadStatistics(0, 0) {
@Override
public int computeIdleExecutors() {
return Label.this.getIdleExecutors();
}
@Override
public int computeTotalExecutors() {
return Label.this.getTotalExecutors();
}
@Override
public int computeQueueLength() {
return Hudson.getInstance().getQueue().countBuildableItemsFor(Label.this);
}
};
this.nodeProvisioner = new NodeProvisioner(this, loadStatistics);
}
/**
* Alias for {@link #getDisplayName()}.
*/
@Exported
public final String getName() {
return getDisplayName();
}
/**
* Returns a human-readable text that represents this label.
*/
public String getDisplayName() {
return name;
}
/**
* Returns a label expression that represents this label.
*/
public abstract String getExpression();
/**
* Relative URL from the context path, that ends with '/'.
*/
public String getUrl() {
return "label/" + name + '/';
}
public String getSearchUrl() {
return getUrl();
}
/**
* Evaluates whether the label expression is true given the specified value
* assignment. IOW, returns true if the assignment provided by the resolver
* matches this label expression.
*/
public abstract boolean matches(VariableResolver<Boolean> resolver);
/**
* Evaluates whether the label expression is true when an entity owns the
* given set of {@link LabelAtom}s.
*/
public final boolean matches(final Collection<LabelAtom> labels) {
return matches(new VariableResolver<Boolean>() {
public Boolean resolve(String name) {
for (LabelAtom a : labels) {
if (a.getName().equals(name)) {
return true;
}
}
return false;
}
});
}
public final boolean matches(Node n) {
return matches(n.getAssignedLabels());
}
/**
* Returns true if this label is a "self label", which means the label is
* the name of a {@link Node}.
*/
public boolean isSelfLabel() {
Set<Node> nodes = getNodes();
return nodes.size() == 1 && nodes.iterator().next().getSelfLabel() == this;
}
/**
* Gets all {@link Node}s that belong to this label.
*/
@Exported
public synchronized Set<Node> getNodes() {
Set<Node> nodes = this.nodes;
if (nodes != null) {
return nodes;
}
Set<Node> r = new HashSet<Node>();
Hudson h = Hudson.getInstance();
if (this.matches(h)) {
r.add(h);
}
for (Node n : h.getNodes()) {
if (this.matches(n)) {
r.add(n);
}
}
return this.nodes = Collections.unmodifiableSet(r);
}
/**
* Gets all {@link Cloud}s that can launch for this label.
*/
@Exported
public synchronized Set<Cloud> getClouds() {
if (clouds == null) {
Set<Cloud> r = new HashSet<Cloud>();
Hudson h = Hudson.getInstance();
for (Cloud c : h.clouds) {
if (c.canProvision(this)) {
r.add(c);
}
}
clouds = Collections.unmodifiableSet(r);
}
return clouds;
}
/**
* Can jobs be assigned to this label? <p> The answer is yes if there is a
* reasonable basis to believe that Hudson can have an executor under this
* label, given the current configuration. This includes situations such as
* (1) there are offline slaves that have this label (2) clouds exist that
* can provision slaves that have this label.
*/
public boolean isAssignable() {
for (Node n : getNodes()) {
if (n.getNumExecutors() > 0) {
return true;
}
}
return !getClouds().isEmpty();
}
/**
* Number of total {@link Executor}s that belong to this label. <p> This
* includes executors that belong to offline nodes, so the result can be
* thought of as a potential capacity, whereas {@link #getTotalExecutors()}
* is the currently functioning total number of executors. <p> This method
* doesn't take the dynamically allocatable nodes (via {@link Cloud}) into
* account. If you just want to test if there's some executors, use
* {@link #isAssignable()}.
*/
public int getTotalConfiguredExecutors() {
int r = 0;
for (Node n : getNodes()) {
r += n.getNumExecutors();
}
return r;
}
/**
* Number of total {@link Executor}s that belong to this label that are
* functioning. <p> This excludes executors that belong to offline nodes.
*/
@Exported
public int getTotalExecutors() {
int r = 0;
for (Node n : getNodes()) {
Computer c = n.toComputer();
if (c != null && c.isOnline()) {
r += c.countExecutors();
}
}
return r;
}
/**
* Number of busy {@link Executor}s that are carrying out some work right
* now.
*/
@Exported
public int getBusyExecutors() {
int r = 0;
for (Node n : getNodes()) {
Computer c = n.toComputer();
if (c != null && c.isOnline()) {
r += c.countBusy();
}
}
return r;
}
/**
* Number of idle {@link Executor}s that can start working immediately.
*/
@Exported
public int getIdleExecutors() {
int r = 0;
for (Node n : getNodes()) {
Computer c = n.toComputer();
if (c != null && (c.isOnline() || c.isConnecting())) {
r += c.countIdle();
}
}
return r;
}
/**
* Returns true if all the nodes of this label is offline.
*/
@Exported
public boolean isOffline() {
for (Node n : getNodes()) {
if (n.toComputer() != null && !n.toComputer().isOffline()) {
return false;
}
}
return true;
}
/**
* Returns a human readable text that explains this label.
*/
@Exported
public String getDescription() {
Set<Node> nodes = getNodes();
if (nodes.isEmpty()) {
Set<Cloud> clouds = getClouds();
if (clouds.isEmpty()) {
return Messages.Label_InvalidLabel();
}
return Messages.Label_ProvisionedFrom(toString(clouds));
}
if (nodes.size() == 1) {
return nodes.iterator().next().getNodeDescription();
}
return Messages.Label_GroupOf(toString(nodes));
}
private String toString(Collection<? extends ModelObject> model) {
boolean first = true;
StringBuilder buf = new StringBuilder();
for (ModelObject c : model) {
if (buf.length() > 80) {
buf.append(",...");
break;
}
if (!first) {
buf.append(',');
} else {
first = false;
}
buf.append(c.getDisplayName());
}
return buf.toString();
}
/**
* Returns projects that are tied on this node.
*/
@Exported
public List<AbstractProject> getTiedJobs() {
List<AbstractProject> r = new ArrayList<AbstractProject>();
for (AbstractProject p : Util.filter(Hudson.getInstance().getItems(), AbstractProject.class)) {
if (this.equals(p.getAssignedLabel())) {
r.add(p);
}
}
for (MatrixProject p : Util.filter(Hudson.getInstance().getItems(), MatrixProject.class)) {
for (Axis axis : p.getAxes()) {
if (axis.getValues().contains(getName())){
r.add(p);
}
}
}
return r;
}
public boolean contains(Node node) {
return getNodes().contains(node);
}
/**
* If there's no such label defined in {@link Node} or {@link Cloud}. This
* is usually used as a signal that this label is invalid.
*/
public boolean isEmpty() {
return getNodes().isEmpty() && getClouds().isEmpty();
}
/*package*/ void reset() {
nodes = null;
clouds = null;
}
/**
* Expose this object to the remote API.
*/
public Api getApi() {
return new Api(this);
}
/**
* Returns the label that represents "this&rhs"
*/
public Label and(Label rhs) {
return new LabelExpression.And(this, rhs);
}
/**
* Returns the label that represents "this|rhs"
*/
public Label or(Label rhs) {
return new LabelExpression.Or(this, rhs);
}
/**
* Returns the label that represents "this<->rhs"
*/
public Label iff(Label rhs) {
return new LabelExpression.Iff(this, rhs);
}
/**
* Returns the label that represents "this->rhs"
*/
public Label implies(Label rhs) {
return new LabelExpression.Implies(this, rhs);
}
/**
* Returns the label that represents "!this"
*/
public Label not() {
return new LabelExpression.Not(this);
}
/**
* Returns the label that represents "(this)" This is a pointless operation
* for machines, but useful for humans who find the additional parenthesis
* often useful
*/
public Label paren() {
return new LabelExpression.Paren(this);
}
/**
* Precedence of the top most operator.
*/
public abstract LabelOperatorPrecedence precedence();
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null || getClass() != that.getClass()) {
return false;
}
return name.equals(((Label) that).name);
}
@Override
public int hashCode() {
return name.hashCode();
}
public int compareTo(Label that) {
return this.name.compareTo(that.name);
}
@Override
public String toString() {
return name;
}
public static final class ConverterImpl implements Converter {
public ConverterImpl() {
}
public boolean canConvert(Class type) {
return Label.class.isAssignableFrom(type);
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
Label src = (Label) source;
writer.setValue(src.getExpression());
}
public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
return Hudson.getInstance().getLabel(reader.getValue());
}
}
/**
* Convers a whitespace-separate list of tokens into a set of
* {@link Label}s.
*
* @param labels Strings like "abc def ghi". Can be empty or null.
* @return Can be empty but never null. A new writable set is always
* returned, so that the caller can add more to the set.
* @since 1.308
*/
public static Set<LabelAtom> parse(String labels) {
Set<LabelAtom> r = new TreeSet<LabelAtom>();
labels = fixNull(labels);
if (labels.length() > 0) {
for (String l : new QuotedStringTokenizer(labels).toArray()) {
r.add(Hudson.getInstance().getLabelAtom(l));
}
}
return r;
}
/**
* Obtains a label by its {@linkplain #getName() name}.
*/
public static Label get(String l) {
return Hudson.getInstance().getLabel(l);
}
/**
* Parses the expression into a label expression tree.
*
* TODO: replace this with a real parser later
*/
public static Label parseExpression(String labelExpression) throws RecognitionException {
LabelExpressionLexer lexer = new LabelExpressionLexer(new ANTLRStringStream(labelExpression));
return new LabelExpressionParser(new CommonTokenStream(lexer)).expr();
}
}