/*******************************************************************************
*
* Copyright (c) 2004-2009 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, Winston Prakash
*
*******************************************************************************/
package hudson.matrix;
import hudson.Util;
import hudson.model.Hudson;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import static java.lang.Boolean.TRUE;
/**
* A particular combination of {@link Axis} values.
*
* For example, when axes are "x={1,2},y={3,4}", then [x=1,y=3] is a combination
* (out of 4 possible combinations)
*
* @author Kohsuke Kawaguchi
*/
public final class Combination extends TreeMap<String, String> implements Comparable<Combination> {
protected static final String DELIM = ",";
public Combination(AxisList axisList, List<String> values) {
for (int i = 0; i < axisList.size(); i++) {
super.put(axisList.get(i).getName(), values.get(i));
}
}
public Combination(AxisList axisList, String... values) {
this(axisList, Arrays.asList(values));
}
public Combination(Map<String, String> keyValuePairs) {
for (Map.Entry<String, String> e : keyValuePairs.entrySet()) {
super.put(e.getKey(), e.getValue());
}
}
public String get(Axis a) {
return get(a.getName());
}
/**
* Obtains the continuous unique index number of this {@link Combination} in
* the given {@link AxisList}.
*/
public int toIndex(AxisList axis) {
int r = 0;
for (Axis a : axis) {
r *= a.size();
r += a.indexOf(get(a));
}
return r;
}
/**
* Obtains a number N such that "N%M==0" would create a reasonable sparse
* matrix for integer M.
*
* <p> This is bit like {@link #toIndex(AxisList)}, but instead of creating
* a continuous number (which often maps different values of the same axis
* to the same index in modulo N residue ring, we use a prime number P as
* the base. I think this guarantees the uniform distribution in any N
* smaller than 2P (but proof, anyone?)
*/
private long toModuloIndex(AxisList axis) {
long r = 0;
for (Axis a : axis) {
r += a.indexOf(get(a));
r *= 31;
}
return r;
}
/**
* Evaluates the given Dynamic Language Script Expression with values bound
* from this combination. <p> For example, if this combination is a=X,b=Y,
* then expressions like <tt>a=="X"</tt> would evaluate to true.
*
* @param axes
* @param expression
* @param scriptType
* @return
*/
public boolean evalScriptExpression(AxisList axes, String expression) {
if (Util.fixEmptyAndTrim(expression) == null) {
return true;
}
Object result = Boolean.TRUE;
if (Hudson.getInstance().getScriptSupport() != null) {
expression = "use(" + BooleanCategory.class.getName().replace('$', '.') + ") {" + expression + "}";
Map<String, Object> variableMap = new HashMap<String, Object>();
for (Map.Entry<String, String> e : entrySet()) {
variableMap.put(e.getKey(), e.getValue());
}
variableMap.put("index", toModuloIndex(axes));
variableMap.put("uniqueId", toIndex(axes));
result = Hudson.getInstance().getScriptSupport().evaluateExpression(expression, variableMap);
}
return TRUE.equals(result);
}
public int compareTo(Combination that) {
int d = this.size() - that.size();
if (d != 0) {
return d;
}
Iterator<Map.Entry<String, String>> itr = this.entrySet().iterator();
Iterator<Map.Entry<String, String>> jtr = that.entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, String> i = itr.next();
Map.Entry<String, String> j = jtr.next();
d = i.getKey().compareTo(j.getKey());
if (d != 0) {
return d;
}
d = i.getValue().compareTo(j.getValue());
if (d != 0) {
return d;
}
}
return 0;
}
/**
* Works like {@link #toString()} but only include the given axes.
*/
public String toString(Collection<Axis> subset) {
if (size() == 1 && subset.size() == 1) {
return values().iterator().next();
}
StringBuilder buf = new StringBuilder();
for (Axis a : subset) {
if (buf.length() > 0) {
buf.append(',');
}
buf.append(a.getName()).append('=').append(get(a));
}
if (buf.length() == 0) {
buf.append("default"); // special case to avoid 0-length name.
}
return buf.toString();
}
/**
* Gets the values that correspond to the specified axes, in their order.
*/
public List<String> values(Collection<? extends Axis> axes) {
List<String> r = new ArrayList<String>(axes.size());
for (Axis a : axes) {
r.add(get(a));
}
return r;
}
/**
* Converts to the ID string representation:
* <tt>axisName=value,axisName=value,...</tt>
*
* @param sep1 The separator between multiple axes.
* @param sep2 The separator between axis name and value.
*/
public String toString(char sep1, char sep2) {
return toString(sep1, sep2, false);
}
/**
* Converts to the ID string representation:
* <tt>axisName=value,axisName=value,...</tt>
*
* @param sep1 The separator between multiple axes.
* @param sep2 The separator between axis name and value.
* @param encodeValue true to encode value {@link Util#rawEncode(String)}
* @return string representation.
*/
public String toString(char sep1, char sep2, boolean encodeValue) {
StringBuilder builder = new StringBuilder();
if (encodeValue) {
for (Map.Entry<String, String> e : entrySet()) {
if (builder.length() > 0) {
builder.append(sep1);
}
builder.append(e.getKey()).append(sep2).append(Util.rawEncode(e.getValue()));
}
} else {
for (Map.Entry<String, String> e : entrySet()) {
if (builder.length() > 0) {
builder.append(sep1);
}
builder.append(e.getKey()).append(sep2).append(e.getValue());
}
}
if (builder.length() == 0) {
builder.append("default"); // special case to avoid 0-length name.
}
return builder.toString();
}
@Override
public String toString() {
return toString(',', '=');
}
/**
* Gets the 8 character-wide hash code for this combination
*/
public String digest() {
return Util.getDigestOf(toString());
}
/**
* Reverse operation of {@link #toString()}.
*/
public static Combination fromString(String id) {
if (id.equals("default")) {
return new Combination(Collections.<String, String>emptyMap());
}
Map<String, String> m = new HashMap<String, String>();
StringTokenizer tokens = new StringTokenizer(id, DELIM);
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken();
int idx = token.indexOf('=');
if (idx < 0) {
throw new IllegalArgumentException("Can't parse " + id);
}
m.put(token.substring(0, idx), token.substring(idx + 1));
}
return new Combination(m);
}
/**
* Creates compact string representataion suitable for display purpose.
*
* <p> The string is made compact by looking for {@link Axis} whose values
* are unique, and omit the axis name.
*/
public String toCompactString(AxisList axes) {
Set<String> nonUniqueAxes = new HashSet<String>();
Map<String, Axis> axisByValue = new HashMap<String, Axis>();
for (Axis a : axes) {
for (String v : a.getValues()) {
Axis old = axisByValue.put(v, a);
if (old != null) {
// these two axes have colliding values
nonUniqueAxes.add(old.getName());
nonUniqueAxes.add(a.getName());
}
}
}
StringBuilder buf = new StringBuilder();
for (Map.Entry<String, String> e : entrySet()) {
if (buf.length() > 0) {
buf.append(',');
}
if (nonUniqueAxes.contains(e.getKey())) {
buf.append(e.getKey()).append('=');
}
buf.append(e.getValue());
}
if (buf.length() == 0) {
buf.append("default"); // special case to avoid 0-length name.
}
return buf.toString();
}
// read-only
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public void putAll(Map<? extends String, ? extends String> map) {
throw new UnsupportedOperationException();
}
@Override
public String put(String key, String value) {
throw new UnsupportedOperationException();
}
@Override
public String remove(Object key) {
throw new UnsupportedOperationException();
}
/**
* Duck-typing for boolean expressions.
*
* @see Combination#evalScriptExpression(AxisList,String)
*/
public static final class BooleanCategory {
/**
* x -> y
*/
public static Boolean implies(Boolean lhs, Boolean rhs) {
return !lhs || rhs;
}
}
}