
hudson.matrix.MatrixProject Maven / Gradle / Ivy
package hudson.matrix;
import hudson.CopyOnWrite;
import hudson.FilePath;
import hudson.XmlFile;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.DependencyGraph;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.Hudson;
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.Node;
import hudson.model.SCMedItem;
import hudson.model.TopLevelItem;
import hudson.model.TopLevelItemDescriptor;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.triggers.Trigger;
import hudson.util.CopyOnWriteMap;
import hudson.util.DescribableList;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
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.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* {@link Job} that allows you to run multiple different configurations
* from a single setting.
*
* @author Kohsuke Kawaguchi
*/
public class MatrixProject extends AbstractProject implements TopLevelItem, SCMedItem, ItemGroup, DescribableList.Owner {
/**
* Other configuration axes.
*
* This also includes special axis "label" and "jdk" if they are configured.
*/
private volatile AxisList axes = new AxisList();
/**
* List of active {@link Builder}s configured for this project.
*/
private DescribableList> builders =
new DescribableList>(this);
/**
* List of active {@link Publisher}s configured for this project.
*/
private DescribableList> publishers =
new DescribableList>(this);
/**
* List of active {@link BuildWrapper}s configured for this project.
*/
private DescribableList> buildWrappers =
new DescribableList>(this);
/**
* All {@link MatrixConfiguration}s, keyed by their {@link MatrixConfiguration#getName() names}.
*/
private transient /*final*/ Map configurations = new CopyOnWriteMap.Tree();
/**
* @see #getActiveConfigurations()
*/
@CopyOnWrite
private transient /*final*/ Set activeConfigurations = new LinkedHashSet();
public MatrixProject(String name) {
super(Hudson.getInstance(), name);
}
public AxisList getAxes() {
return axes;
}
protected void updateTransientActions() {
synchronized(transientActions) {
super.updateTransientActions();
for (BuildStep step : builders) {
Action a = step.getProjectAction(this);
if(a!=null)
transientActions.add(a);
}
for (BuildStep step : publishers) {
Action a = step.getProjectAction(this);
if(a!=null)
transientActions.add(a);
}
for (BuildWrapper step : buildWrappers) {
Action a = step.getProjectAction(this);
if(a!=null)
transientActions.add(a);
}
for (Trigger trigger : triggers) {
Action a = trigger.getProjectAction();
if(a!=null)
transientActions.add(a);
}
}
}
/**
* Gets the subset of {@link AxisList} that are not system axes.
*/
public List getUserAxes() {
List r = new ArrayList();
for (Axis a : axes)
if(!a.isSystem())
r.add(a);
return r;
}
public Layouter getLayouter() {
return new Layouter(axes) {
protected MatrixConfiguration getT(Combination c) {
return getItem(c);
}
};
}
public void onLoad(ItemGroup extends Item> parent, String name) throws IOException {
super.onLoad(parent,name);
Collections.sort(axes); // perhaps the file was edited on disk and the sort order might have been broken
builders.setOwner(this);
publishers.setOwner(this);
buildWrappers.setOwner(this);
rebuildConfigurations();
}
public void logRotate() throws IOException {
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
*
*
* The directory structure would be axis-a/b/axis-c/d/axis-e/f 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 result, Map 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 c = new HashMap(combination);
c.put(axis,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}.
*/
private void rebuildConfigurations() 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());
dir.renameTo(getRootDirFor(c));
} catch (IllegalArgumentException e) {
// it's not a configuration dir. Just ignore.
}
}
}
}
CopyOnWriteMap.Tree configurations =
new CopyOnWriteMap.Tree();
loadConfigurations(getConfigurationsDir(),configurations,Collections.emptyMap());
this.configurations = configurations;
// find all active configurations
Set active = new LinkedHashSet();
for (Combination c : axes.list()) {
MatrixConfiguration config = configurations.get(c);
if(config==null) {
config = new MatrixConfiguration(this,c);
config.save();
configurations.put(config.getCombination(), config);
}
active.add(config);
}
this.activeConfigurations = active;
}
private File getConfigurationsDir() {
return new File(getRootDir(),"configurations");
}
/**
* Gets all active configurations.
*
* In contract, inactive configurations are those that are left for archival purpose
* and no longer built when a new {@link MatrixBuild} is executed.
*/
public Collection getActiveConfigurations() {
return activeConfigurations;
}
public Collection getItems() {
return configurations.values();
}
@Override
public Collection extends Job> getAllJobs() {
Set jobs = new HashSet(getItems());
jobs.add(this);
return jobs;
}
public String getUrlChildPrefix() {
return ".";
}
public MatrixConfiguration getItem(String name) {
return getItem(Combination.fromString(name));
}
public MatrixConfiguration getItem(Combination c) {
return configurations.get(c);
}
public File getRootDirFor(MatrixConfiguration child) {
return getRootDirFor(child.getCombination());
}
public File getRootDirFor(Combination combination) {
File f = getConfigurationsDir();
for (Entry e : combination.entrySet())
f = new File(f,"axis-"+e.getKey()+'/'+e.getValue());
f.getParentFile().mkdirs();
return f;
}
public Hudson getParent() {
return Hudson.getInstance();
}
/**
* @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 Set getJDKs() {
Axis a = axes.find("jdk");
if(a==null) return Collections.emptySet();
Set r = new HashSet();
for (String j : a) {
JDK jdk = Hudson.getInstance().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 Set