/*
* The MIT License
*
* Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Jorg Heymans, Red Hat, Inc., id:cactusman
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.matrix;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import hudson.CopyOnWrite;
import hudson.Extension;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.matrix.MatrixBuild.MatrixBuildExecution;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildableItemWithBuildWrappers;
import hudson.model.DependencyGraph;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.JDK;
import hudson.model.Job;
import hudson.model.Label;
import hudson.model.ParameterDefinition;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue.FlyweightTask;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.SCMedItem;
import hudson.model.Saveable;
import hudson.model.TopLevelItem;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.tasks.test.AggregatedTestResultAction;
import hudson.triggers.Trigger;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.CopyOnWriteMap;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import jenkins.scm.SCMCheckoutStrategyDescriptor;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.TokenList;
import org.kohsuke.stapler.export.Exported;
/**
* {@link Job} that allows you to run multiple different configurations
* from a single setting.
*
* @author Kohsuke Kawaguchi
*/
public class MatrixProject extends AbstractProject<MatrixProject,MatrixBuild> implements TopLevelItem, SCMedItem, ItemGroup<MatrixConfiguration>, Saveable, FlyweightTask, BuildableItemWithBuildWrappers {
/**
* Configuration axes.
*/
private volatile AxisList axes = new AxisList();
/**
* The filter that is applied to combinations. It is a Groovy if condition.
* This can be null, which means "true".
*
* @see #getCombinationFilter()
*/
private volatile String combinationFilter;
/**
* List of active {@link Builder}s configured for this project.
*/
private DescribableList<Builder,Descriptor<Builder>> builders =
new DescribableList<Builder,Descriptor<Builder>>(this);
/**
* List of active {@link Publisher}s configured for this project.
*/
private DescribableList<Publisher,Descriptor<Publisher>> publishers =
new DescribableList<Publisher,Descriptor<Publisher>>(this);
/**
* List of active {@link BuildWrapper}s configured for this project.
*/
private DescribableList<BuildWrapper,Descriptor<BuildWrapper>> buildWrappers =
new DescribableList<BuildWrapper,Descriptor<BuildWrapper>>(this);
/**
* All {@link MatrixConfiguration}s, keyed by their {@link MatrixConfiguration#getName() names}.
*/
private transient /*final*/ Map<Combination,MatrixConfiguration> configurations = new CopyOnWriteMap.Tree<Combination,MatrixConfiguration>();
/**
* @see #getActiveConfigurations()
*/
@CopyOnWrite
private transient /*final*/ Set<MatrixConfiguration> activeConfigurations = new LinkedHashSet<MatrixConfiguration>();
/**
* @deprecated as of 1.456
* Moved to {@link DefaultMatrixExecutionStrategyImpl}
*/
@Deprecated
private transient Boolean runSequentially;
/**
* Filter to select a number of combinations to build first
* @deprecated as of 1.456
* Moved to {@link DefaultMatrixExecutionStrategyImpl}
*/
@Deprecated
private transient String touchStoneCombinationFilter;
/**
* Required result on the touchstone combinations, in order to
* continue with the rest
* @deprecated as of 1.456
* Moved to {@link DefaultMatrixExecutionStrategyImpl}
*/
@Deprecated
private transient Result touchStoneResultCondition;
/**
* @deprecated as of 1.456
* Moved to {@link DefaultMatrixExecutionStrategyImpl}
*/
@Deprecated
private transient MatrixConfigurationSorter sorter;
private MatrixExecutionStrategy executionStrategy;
/**
* Custom workspace location for {@link MatrixConfiguration}s.
*
* <p>
* (Historically, we used {@link AbstractProject#customWorkspace} + some unique suffix (see {@link MatrixConfiguration#useShortWorkspaceName})
* for custom workspace, but we now separated that so that the user has more control.
*
* <p>
* If null, the historical semantics is assumed.
*
* @since 1.466
*/
private String childCustomWorkspace;
/**
* Lock to prevent project changes on different build at the same time
*/
private transient Lock buildLock = new ReentrantLock();
public MatrixProject(String name) {
this(Jenkins.getInstance(), name);
}
public MatrixProject(ItemGroup parent, String name) {
super(parent, name);
}
protected Object readResolve() {
buildLock = new ReentrantLock();
return this;
}
@Override
public String getPronoun() {
return AlternativeUiTextProvider.get(PRONOUN, this, Messages.MatrixProject_Pronoun());
}
/**
* Gets the workspace location that {@link MatrixConfiguration} uses.
*
* Used from {@code MatrixRun.MatrixRunExecution.decideWorkspace}.
*
* @return never null
* even when {@link MatrixProject} uses no custom workspace, this method still
* returns something like "${PARENT_WORKSPACE}/${COMBINATION}" that controls
* how the workspace should be laid out.
*
* The return value can be absolute or relative. If relative, it is resolved
* against the working directory of the overarching {@link MatrixBuild}.
*/
public String getChildCustomWorkspace() {
String ws = childCustomWorkspace;
if (ws==null) {
ws = MatrixConfiguration.useShortWorkspaceName?"${SHORT_COMBINATION}":"${COMBINATION}";
}
return ws;
}
/**
* Do we have an explicit child custom workspace setting (true)? Or just using the default value (false)?
*/
public boolean hasChildCustomWorkspace() {
return childCustomWorkspace!=null;
}
public void setChildCustomWorkspace(String childCustomWorkspace) throws IOException {
this.childCustomWorkspace = Util.fixEmptyAndTrim(childCustomWorkspace);
save();
}
/**
* {@link MatrixProject} is relevant with all the labels its configurations are relevant.
*/
@Override
public Set<Label> getRelevantLabels() {
Set<Label> r = new HashSet<Label>();
for (MatrixConfiguration c : getActiveConfigurations())
r.add(c.getAssignedLabel());
return r;
}
/**
* @return can be null (to indicate that the configurations should be left to their natural order.)
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#getSorter()}.
* This method tries to emulate the previous behavior the best it can, but will return null
* if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public MatrixConfigurationSorter getSorter() {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
return dm.getSorter();
}
return null;
}
/**
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#setSorter(MatrixConfigurationSorter)}.
* This method tries to emulate the previous behavior the best it can, but will fall back
* to no-op if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public void setSorter(MatrixConfigurationSorter sorter) throws IOException {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
dm.setSorter(sorter);
save();
}
}
public AxisList getAxes() {
return axes;
}
/**
* Reconfigures axes.
*/
public void setAxes(AxisList axes) throws IOException {
this.axes = new AxisList(axes);
rebuildConfigurations(null);
save();
}
public MatrixExecutionStrategy getExecutionStrategy() {
return executionStrategy;
}
public void setExecutionStrategy(MatrixExecutionStrategy executionStrategy) throws IOException {
if (executionStrategy ==null) throw new IllegalArgumentException();
this.executionStrategy = executionStrategy;
save();
}
/**
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#isRunSequentially()}.
* This method tries to emulate the previous behavior the best it can, but will return false
* if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public boolean isRunSequentially() {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
return dm.isRunSequentially();
}
return false;
}
/**
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#setRunSequentially(boolean)}.
* This method tries to emulate the previous behavior the best it can, but will fall back
* to no-op if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public void setRunSequentially(boolean runSequentially) throws IOException {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
dm.setRunSequentially(runSequentially);
save();
}
}
/**
* Sets the combination filter.
*
* @param combinationFilter the combinationFilter to set
*/
public void setCombinationFilter(String combinationFilter) throws IOException {
this.combinationFilter = combinationFilter;
rebuildConfigurations(null);
save();
}
/**
* Obtains the combination filter, used to trim down the size of the matrix.
*
* <p>
* By default, a {@link MatrixConfiguration} is created for every possible combination of axes exhaustively.
* But by specifying a Groovy expression as a combination filter, one can trim down the # of combinations built.
*
* <p>
* Namely, this expression is evaluated for each axis value combination, and only when it evaluates to true,
* a corresponding {@link MatrixConfiguration} will be created and built.
*
* @return can be null.
* @since 1.279
*/
public String getCombinationFilter() {
return combinationFilter;
}
/**
* @return can be null (to indicate that the configurations should be left to their natural order.)
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#getTouchStoneCombinationFilter()}.
* This method tries to emulate the previous behavior the best it can, but will return null
* if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public String getTouchStoneCombinationFilter() {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
return dm.getTouchStoneCombinationFilter();
}
return null;
}
/**
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#setTouchStoneCombinationFilter(String)}.
* This method tries to emulate the previous behavior the best it can, but will fall back
* to no-op if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public void setTouchStoneCombinationFilter(String touchStoneCombinationFilter) throws IOException {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
dm.setTouchStoneCombinationFilter(touchStoneCombinationFilter);
save();
}
}
/**
* @return can be null (to indicate that the configurations should be left to their natural order.)
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#getTouchStoneResultCondition()}.
* This method tries to emulate the previous behavior the best it can, but will return null
* if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public Result getTouchStoneResultCondition() {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
return dm.getTouchStoneResultCondition();
}
return null;
}
/**
* @deprecated as of 1.456
* Use {@link DefaultMatrixExecutionStrategyImpl#setTouchStoneResultCondition(Result)}.
* This method tries to emulate the previous behavior the best it can, but will fall back
* to no-op if the current {@link MatrixExecutionStrategy} is not the default one.
*/
@Deprecated
public void setTouchStoneResultCondition(Result touchStoneResultCondition) throws IOException {
MatrixExecutionStrategy e = executionStrategy;
if (e instanceof DefaultMatrixExecutionStrategyImpl) {
DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e;
dm.setTouchStoneResultCondition(touchStoneResultCondition);
save();
}
}
@Override
protected List<Action> createTransientActions() {
List<Action> r = super.createTransientActions();
for (BuildStep step : builders)
r.addAll(step.getProjectActions(this));
for (BuildStep step : publishers)
r.addAll(step.getProjectActions(this));
for (BuildWrapper step : buildWrappers)
r.addAll(step.getProjectActions(this));
for (Trigger<?> trigger : triggers())
r.addAll(trigger.getProjectActions());
return r;
}
@Override
protected void updateTransientActions(){
super.updateTransientActions();
if(getActiveConfigurations() !=null){
// update all transient actions in configurations too.
for(MatrixConfiguration configuration: getActiveConfigurations()){
configuration.updateTransientActions();
}
}
}
/**
* Gets the subset of {@link AxisList} that are not system axes.
*
* @deprecated as of 1.373
* System vs user difference are generalized into extension point.
*/
@Deprecated
public List<Axis> getUserAxes() {
List<Axis> r = new ArrayList<Axis>();
for (Axis a : axes)
if(!a.isSystem())
r.add(a);
return r;
}
public Layouter<MatrixConfiguration> getLayouter() {
return new Layouter<MatrixConfiguration>(axes) {
@Override
protected MatrixConfiguration getT(Combination c) {
return getItem(c);
}
};
}
@Override
public void onCreatedFromScratch() {
executionStrategy = new DefaultMatrixExecutionStrategyImpl();
super.onCreatedFromScratch();
}
@Override
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
super.onLoad(parent,name);
builders.setOwner(this);
publishers.setOwner(this);
buildWrappers.setOwner(this);
if (executionStrategy ==null)
executionStrategy = new DefaultMatrixExecutionStrategyImpl(runSequentially != null ? runSequentially : false, touchStoneCombinationFilter, touchStoneResultCondition, sorter);
rebuildConfigurations(null);
}
@Override
public void logRotate() throws IOException, InterruptedException {
super.logRotate();
// perform the log rotation of inactive configurations to make sure
// their logs get eventually discarded
for (MatrixConfiguration config : configurations.values()) {
if(!config.isActiveConfiguration())
config.logRotate();
}
}
/**
* Recursively search for configuration and put them to the map
*
* <p>
* The directory structure would be <tt>axis-a/b/axis-c/d/axis-e/f</tt> for
* combination [a=b,c=d,e=f]. Note that two combinations [a=b,c=d] and [a=b,c=d,e=f]
* can both co-exist (where one is an archived record and the other is live, for example)
* so search needs to be thorough.
*
* @param dir
* Directory to be searched.
* @param result
* Receives the loaded {@link MatrixConfiguration}s.
* @param combination
* Combination of key/values discovered so far while traversing the directories.
* Read-only.
*/
private void loadConfigurations( File dir, CopyOnWriteMap.Tree<Combination,MatrixConfiguration> result, Map<String,String> combination ) {
File[] axisDirs = dir.listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory() && child.getName().startsWith("axis-");
}
});
if(axisDirs==null) return;
for (File subdir : axisDirs) {
String axis = subdir.getName().substring(5); // axis name
File[] valuesDir = subdir.listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory();
}
});
if(valuesDir==null) continue; // no values here
for (File v : valuesDir) {
Map<String,String> c = new HashMap<String, String>(combination);
c.put(axis,TokenList.decode(v.getName()));
try {
XmlFile config = Items.getConfigFile(v);
if(config.exists()) {
Combination comb = new Combination(c);
// if we already have this in memory, just use it.
// otherwise load it
MatrixConfiguration item=null;
if(this.configurations!=null)
item = this.configurations.get(comb);
if(item==null) {
item = (MatrixConfiguration) config.read();
item.setCombination(comb);
item.onLoad(this, v.getName());
}
result.put(item.getCombination(), item);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load matrix configuration "+v,e);
}
loadConfigurations(v,result,c);
}
}
}
/**
* Rebuilds the {@link #configurations} list and {@link #activeConfigurations}.
*
* @param context
* We rebuild configurations right before a build, to allow configurations to be adjusted for the build.
* (think of it as reconfiguring a project right before a build.) And when that happens, this value is the
* build in progress. Otherwise this value is null (for example, when Jenkins is booting up.)
*/
/*package*/ Set<MatrixConfiguration> rebuildConfigurations(MatrixBuildExecution context) throws IOException {
{
// backward compatibility check to see if there's any data in the old structure
// if so, bring them to the newer structure.
File[] oldDirs = getConfigurationsDir().listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory() && !child.getName().startsWith("axis-");
}
});
if(oldDirs!=null) {
// rename the old directory to the new one
for (File dir : oldDirs) {
try {
Combination c = Combination.fromString(dir.getName());
//TODO: bad logic in the legacy code, creating a directory and then replacing it
final File target = getRootDirFor(c);
if (!dir.renameTo(target)) {
LOGGER.log(Level.WARNING, "Cannot rename directory {0} to {1}", new Object[]{dir, target});
}
} catch (IllegalArgumentException e) {
// it's not a configuration dir. Just ignore.
}
}
}
}
CopyOnWriteMap.Tree<Combination,MatrixConfiguration> configurations =
new CopyOnWriteMap.Tree<Combination,MatrixConfiguration>();
loadConfigurations(getConfigurationsDir(),configurations,Collections.<String,String>emptyMap());
this.configurations = configurations;
Iterable<Combination> activeCombinations;
if (context!=null) {
List<Set<String>> axesList = Lists.newArrayList();
for (Axis axis : axes)
axesList.add(Sets.newLinkedHashSet(axis.rebuild(context)));
activeCombinations = Iterables.transform(Sets.cartesianProduct(axesList), new Function<List<String>, Combination>() {
public Combination apply(@Nullable List<String> strings) {
assert strings != null;
return new Combination(axes, strings);
}
});
} else {
activeCombinations = axes.list();
}
// find all active configurations
final Set<MatrixConfiguration> active = new LinkedHashSet<MatrixConfiguration>();
final boolean isDynamicFilter = isDynamicFilter(getCombinationFilter());
for (Combination c : activeCombinations) {
if(isDynamicFilter || c.evalGroovyExpression(axes,getCombinationFilter())) {
LOGGER.fine("Adding configuration: " + c);
MatrixConfiguration config = configurations.get(c);
if(config==null) {
config = new MatrixConfiguration(this,c);
config.onCreatedFromScratch();
config.save();
configurations.put(config.getCombination(), config);
}
active.add(config);
}
}
this.activeConfigurations = active;
return active;
}
/**
* Configuration for matrix build
*/
/*package*/ static class RunConfiguration {
public Set<MatrixConfiguration> config;
public AxisList axisList;
}
/**
* Rebuild project settings and return actual configuration and axis list
*/
/*package*/ RunConfiguration getRunConfiguration(MatrixBuildExecution context) throws IOException {
RunConfiguration runConfig = new RunConfiguration();
try {
buildLock.lock();
// give axes a chance to rebuild themselves
runConfig.config = rebuildConfigurations(context);
// deep copy the axes
runConfig.axisList = (AxisList) Jenkins.XSTREAM.fromXML(Jenkins.XSTREAM.toXML(axes));
} finally {
buildLock.unlock();
}
return runConfig;
}
private boolean isDynamicFilter(final String filter) {
if (!isParameterized() || filter == null) return false;
final ParametersDefinitionProperty paramDefProp = getProperty(ParametersDefinitionProperty.class);
for (final ParameterDefinition definition : paramDefProp.getParameterDefinitions()) {
final String name = definition.getName();
final Matcher matcher = Pattern
.compile("\\b" + name + "\\b")
.matcher(filter)
;
if (matcher.find()) return true;
}
return false;
}
private File getConfigurationsDir() {
return new File(getRootDir(),"configurations");
}
/**
* Gets all active configurations.
*
* <p>
* In contract, inactive configurations are those that are left for archival purpose
* and no longer built when a new {@link MatrixBuild} is executed.
*
* <p>
* During a build, {@link MatrixBuildExecution#getActiveConfigurations()} should be used
* to make sure that a build is using the consistent set of active configurations from
* the start to the end.
*/
@Exported
public Collection<MatrixConfiguration> getActiveConfigurations() {
return activeConfigurations;
}
public Collection<MatrixConfiguration> getItems() {
return configurations.values();
}
@Override
public Collection<? extends Job> getAllJobs() {
Set<Job> jobs = new HashSet<Job>(getItems());
jobs.add(this);
return jobs;
}
public String getUrlChildPrefix() {
return ".";
}
public MatrixConfiguration getItem(String name) {
try {
return getItem(Combination.fromString(name));
} catch (IllegalArgumentException e) {
return null;
}
}
public MatrixConfiguration getItem(Combination c) {
if (configurations == null) {
return null;
}
return configurations.get(c);
}
/**
* Gets a root directory of the specified {@link MatrixConfiguration}.
* Creates the whole directory hierarchy on-demand.
* @param child child {@link MatrixConfiguration}
* @return Root directory for the combination
*/
@Nonnull
public File getRootDirFor(@Nonnull MatrixConfiguration child) {
return getRootDirFor(child.getCombination());
}
public void onRenamed(MatrixConfiguration item, String oldName, String newName) throws IOException {
throw new UnsupportedOperationException();
}
public void onDeleted(MatrixConfiguration item) throws IOException {
if(activeConfigurations.contains(item)){
LOGGER.warning("Trying to delete active configuration " + item.getDisplayName() + " of job " + getDisplayName() + ". Active configurations should not be deleted.");
}
else{
configurations.remove(item.getCombination());
}
}
/**
* Gets a root directory for the specified {@link Combination}.
* Creates the whole directory hierarchy on-demand.
* @param combination Combination to be checked
* @return Root directory for the combination
*/
@Nonnull
public File getRootDirFor(@Nonnull Combination combination) {
File f = getConfigurationsDir();
for (Entry<String, String> e : combination.entrySet())
f = new File(f,"axis-"+e.getKey()+'/'+Util.rawEncode(e.getValue()));
if (!f.getParentFile().exists() && !f.getParentFile().mkdirs()) {
LOGGER.log(Level.WARNING, "Cannot create directory {0} for the combination {1}", new Object[]{f, combination});
}
return f;
}
/**
* @see #getJDKs()
*/
@Override @Deprecated
public JDK getJDK() {
return super.getJDK();
}
/**
* Gets the {@link JDK}s where the builds will be run.
* @return never null but can be empty
*/
public @Nonnull Set<JDK> getJDKs() {
final Jenkins jenkins = Jenkins.getInstance();
if (jenkins == null) {
return Collections.emptySet();
}
Axis a = axes.find("jdk");
if(a==null) return Collections.emptySet();
Set<JDK> r = new HashSet<JDK>();
for (String j : a) {
JDK jdk = jenkins.getJDK(j);
if(jdk!=null)
r.add(jdk);
}
return r;
}
/**
* Gets the {@link Label}s where the builds will be run.
* @return never null
*/
public @Nonnull Set<Label> getLabels() {
final Jenkins jenkins = Jenkins.getInstance();
if (jenkins == null) {
return Collections.emptySet();
}
Set<Label> r = new HashSet<Label>();
for (Combination c : axes.subList(LabelAxis.class).list())
r.add(jenkins.getLabel(Util.join(c.values(),"&&")));
return r;
}
public List<Builder> getBuilders() {
return builders.toList();
}
public DescribableList<Builder,Descriptor<Builder>> getBuildersList() {
return builders;
}
public Map<Descriptor<Publisher>,Publisher> getPublishers() {
return publishers.toMap();
}
@Override
public DescribableList<Publisher,Descriptor<Publisher>> getPublishersList() {
return publishers;
}
public DescribableList<BuildWrapper, Descriptor<BuildWrapper>> getBuildWrappersList() {
return buildWrappers;
}
public Map<Descriptor<BuildWrapper>,BuildWrapper> getBuildWrappers() {
return buildWrappers.toMap();
}
public Publisher getPublisher(Descriptor<Publisher> descriptor) {
for (Publisher p : publishers) {
if(p.getDescriptor()==descriptor)
return p;
}
return null;
}
@Override
protected Class<MatrixBuild> getBuildClass() {
return MatrixBuild.class;
}
@Override
public boolean isFingerprintConfigured() {
return false;
}
@Override protected void buildDependencyGraph(DependencyGraph graph) {
super.buildDependencyGraph(graph);
publishers.buildDependencyGraph(this,graph);
builders.buildDependencyGraph(this,graph);
buildWrappers.buildDependencyGraph(this,graph);
}
public MatrixProject asProject() {
return this;
}
@Override
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
try {
MatrixConfiguration item = getItem(token);
if(item!=null)
return item;
} catch (IllegalArgumentException _) {
// failed to parse the token as Combination. Must be something else
}
return super.getDynamic(token,req,rsp);
}
@Override
protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
super.submit(req, rsp);
JSONObject json = req.getSubmittedForm();
if(req.getParameter("hasCombinationFilter")!=null) {
this.combinationFilter = Util.nullify(req.getParameter("combinationFilter"));
} else {
this.combinationFilter = null;
}
if(json.optBoolean("hasChildCustomWorkspace", json.has("childCustomWorkspace"))) {
setChildCustomWorkspace(Util.fixEmptyAndTrim(json.optString("childCustomWorkspace")));
} else {
setChildCustomWorkspace(null);
}
List<MatrixExecutionStrategyDescriptor> esd = getDescriptor().getExecutionStrategyDescriptors();
if (esd.size()>1)
executionStrategy = req.bindJSON(MatrixExecutionStrategy.class,json.getJSONObject("executionStrategy"));
else
executionStrategy = req.bindJSON(esd.get(0).clazz,json.getJSONObject("executionStrategy"));
DescribableList<Axis,AxisDescriptor> newAxes = new DescribableList<Axis,AxisDescriptor>(this);
newAxes.rebuildHetero(req, json, Axis.all(),"axis");
checkAxes(newAxes);
this.axes = new AxisList(newAxes.toList());
buildWrappers.rebuild(req, json, BuildWrappers.getFor(this));
builders.rebuildHetero(req, json, Builder.all(), "builder");
publishers.rebuildHetero(req, json, Publisher.all(), "publisher");
rebuildConfigurations(null);
}
public AggregatedTestResultAction getAggregatedTestResultAction() {
MatrixBuild b = getLastCompletedBuild();
return b != null ? b.getAction(AggregatedTestResultAction.class) : null;
}
/**
* Verifies that Axis names are valid and unique.
*/
private void checkAxes(Iterable<Axis> newAxes) throws FormException {
HashSet<String> axisNames = new HashSet<String>();
for (Axis a : newAxes) {
final AxisDescriptor desc = a.getDescriptor();
FormValidation fv = desc.doCheckName(a.getName());
if (fv.kind!=Kind.OK) {
final String msg = Messages.MatrixProject_InvalidAxisName(a.getName(), fv.getMessage());
throw new FormException(msg,fv,"axis.name");
}
for (String value: a.getValues()) {
fv = desc.checkValue(value);
if (fv.kind!=Kind.OK) {
final String msg = Messages.MatrixProject_InvalidAxisValue(value, fv.getMessage());
// This is done on wrong place, MatrixProject is not supposed
// to know field names of arbitrary axis implementations
throw new FormException(msg,fv,"axis.value");
}
}
if (axisNames.contains(a.getName()))
throw new FormException(Messages.MatrixProject_DuplicateAxisName(),"axis.name");
axisNames.add(a.getName());
}
}
/**
* Also delete all the workspaces of the configuration, too.
*/
@Override
public HttpResponse doDoWipeOutWorkspace() throws IOException, ServletException, InterruptedException {
HttpResponse rsp = super.doDoWipeOutWorkspace();
for (MatrixConfiguration c : configurations.values())
c.doDoWipeOutWorkspace();
return rsp;
}
@Override
public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
ContextMenu menu = new ContextMenu();
for (MatrixConfiguration c : getActiveConfigurations()) {
menu.add(c);
}
return menu;
}
/**
* @throws IllegalStateException Jenkins is not ready
*/
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)Jenkins.getActiveInstance().getDescriptorOrDie(getClass());
}
/**
* Descriptor is instantiated as a field purely for backward compatibility.
* Do not do this in your code. Put @Extension on your DescriptorImpl class instead.
*/
@Restricted(NoExternalUse.class)
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public static final class DescriptorImpl extends AbstractProjectDescriptor {
public String getDisplayName() {
return Messages.MatrixProject_DisplayName();
}
/**
* Needed if it wants Matrix projects are categorized in Jenkins 2.x.
*
* TODO: After Jenkins 2.0 this should be an @Override
*
* @return A string it represents a ItemCategory identifier.
*/
public String getCategoryId() {
return "standalone-projects";
}
/**
* Needed if it wants Matrix projects are categorized in Jenkins 2.x.
*
* TODO: After Jenkins 2.0 this should be an @Override
*
* @return A string with the Item description.
*/
public String getDescription() {
return Messages.MatrixProject_Description();
}
/**
* Needed if it wants Matrix projects are categorized in Jenkins 2.x.
*
* TODO: After Jenkins 2.0 this should be an @Override
*
* @return A string it represents a URL pattern to get the Item icon in different sizes.
*/
public String getIconFilePathPattern() {
return "plugin/matrix-project/images/:size/matrixproject.png";
}
public MatrixProject newInstance(ItemGroup parent, String name) {
return new MatrixProject(parent,name);
}
/**
* All {@link AxisDescriptor}s that contribute to the UI.
*/
public List<AxisDescriptor> getAxisDescriptors() {
List<AxisDescriptor> r = new ArrayList<AxisDescriptor>();
for (AxisDescriptor d : Axis.all()) {
if (d.isInstantiable())
r.add(d);
}
return r;
}
/**
* @deprecated as of 1.456
* This was only exposed for Jelly.
*/
@Deprecated
public List<MatrixConfigurationSorterDescriptor> getSorterDescriptors() {
return MatrixConfigurationSorterDescriptor.all();
}
public List<MatrixExecutionStrategyDescriptor> getExecutionStrategyDescriptors() {
return MatrixExecutionStrategyDescriptor.all();
}
public List<SCMCheckoutStrategyDescriptor> getMatrixRunCheckoutStrategyDescriptors() {
return SCMCheckoutStrategyDescriptor.all();
}
}
private static final Logger LOGGER = Logger.getLogger(MatrixProject.class.getName());
@Initializer(before=InitMilestone.EXTENSIONS_AUGMENTED)
public static void alias() {
Items.XSTREAM.alias("matrix-project", MatrixProject.class);
Items.XSTREAM.alias("axis", Axis.class);
Items.XSTREAM.alias("matrix-config", MatrixConfiguration.class);
Run.XSTREAM.alias("matrix-build",MatrixBuild.class);
Run.XSTREAM.alias("matrix-run",MatrixRun.class);
}
}