diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 30ea3b973989a20685876c7bf973bdbf466bc403..8db131d770393dd502e99144d8d10ac8407ed58c 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -12,6 +12,7 @@ import hudson.model.Job; import hudson.model.JobPropertyDescriptor; import hudson.model.ModelObject; import hudson.model.Node; +import hudson.model.ParameterDefinition; import hudson.model.Project; import hudson.model.Run; import hudson.model.TopLevelItem; @@ -554,6 +555,10 @@ public class Functions { return RetentionStrategy.LIST; } + public static List> getParameterDescriptors() { + return ParameterDefinition.LIST; + } + /** * Computes the path to the icon of the given action * from the context path. diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index b0bdbffce6f3eb569944eb27d333689e742b0049..ce1a6b35358a223b2a89d54d5cd0dc1762d75e90 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -5,6 +5,7 @@ import hudson.model.Hudson; import static hudson.model.Hudson.isWindows; import hudson.util.IOException2; import hudson.util.QuotedStringTokenizer; +import hudson.util.VariableResolver; import hudson.Proc.LocalProc; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; @@ -99,6 +100,16 @@ public class Util { * */ public static String replaceMacro(String s, Map properties) { + return replaceMacro(s,new VariableResolver.ByMap(properties)); + } + + /** + * Replaces the occurrence of '$key' by resolver.get('key'). + * + *

+ * Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.) + */ + public static String replaceMacro(String s, VariableResolver resolver) { int idx=0; while(true) { Matcher m = VARIABLE.matcher(s); @@ -107,7 +118,7 @@ public class Util { String key = m.group().substring(1); if(key.charAt(0)=='{') key = key.substring(1,key.length()-1); - String value = properties.get(key); + String value = resolver.resolve(key); if(value==null) idx = m.start()+1; // skip this else { diff --git a/core/src/main/java/hudson/WebAppMain.java b/core/src/main/java/hudson/WebAppMain.java index a3b63b9365d78f7b323ef7581ce5a2d80326912c..e4c250bad50bc016c6705870f1abfb9bfb607d43 100644 --- a/core/src/main/java/hudson/WebAppMain.java +++ b/core/src/main/java/hudson/WebAppMain.java @@ -36,6 +36,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.Locale; import java.util.Properties; +import java.util.Timer; import java.util.logging.Level; import java.util.logging.Logger; @@ -152,6 +153,8 @@ public class WebAppMain implements ServletContextListener { try { computeVersion(context); + Trigger.timer = new Timer("Hudson cron thread"); + try { context.setAttribute(APP,new Hudson(home,context)); } catch( IOException e ) { diff --git a/core/src/main/java/hudson/maven/EmbedderLoggerImpl.java b/core/src/main/java/hudson/maven/EmbedderLoggerImpl.java index 1190e6394dae39e13bce7b504fdb6e94deff3c0d..8dc8a7da846ec0d4873f476e32b2edfaf7dc0e34 100644 --- a/core/src/main/java/hudson/maven/EmbedderLoggerImpl.java +++ b/core/src/main/java/hudson/maven/EmbedderLoggerImpl.java @@ -13,7 +13,7 @@ import java.util.StringTokenizer; * * @author Kohsuke Kawaguchi */ -final class EmbedderLoggerImpl extends AbstractMavenEmbedderLogger { +public final class EmbedderLoggerImpl extends AbstractMavenEmbedderLogger { private final PrintStream logger; public EmbedderLoggerImpl(TaskListener listener) { diff --git a/core/src/main/java/hudson/maven/MavenModuleSetBuild.java b/core/src/main/java/hudson/maven/MavenModuleSetBuild.java index 0dbc54d64c6e43fa50627caa1e2e0946083bb4db..c0a0478d224a9157ad81fd1a5e7828ef667d6504 100644 --- a/core/src/main/java/hudson/maven/MavenModuleSetBuild.java +++ b/core/src/main/java/hudson/maven/MavenModuleSetBuild.java @@ -17,6 +17,7 @@ import hudson.model.BuildListener; import hudson.model.Fingerprint; import hudson.model.Hudson; import hudson.model.Result; +import hudson.model.ParametersAction; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.util.ArgumentListBuilder; @@ -289,8 +290,15 @@ public final class MavenModuleSetBuild extends AbstractBuild wrappers = new ArrayList(); + for (BuildWrapper w : project.getBuildWrappers()) + wrappers.add(w); + ParametersAction parameters = getAction(ParametersAction.class); + if (parameters != null) + parameters.createBuildWrappers(MavenModuleSetBuild.this,wrappers); + buildEnvironments = new ArrayList(); - for( BuildWrapper w : project.getBuildWrappers()) { + for( BuildWrapper w : wrappers) { BuildWrapper.Environment e = w.setUp((AbstractBuild)MavenModuleSetBuild.this, launcher, listener); if(e==null) return Result.FAILURE; diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index 5342afb5fec1c9e14c2add298aa29545f7015c7c..f92de552942afef0503ce7c2979e6d38755ff927 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -378,10 +378,14 @@ public abstract class AbstractBuild

,R extends Abs jdk.buildEnvVars(env); project.getScm().buildEnvVars(this,env); - if(buildEnvironments!=null) { + ParametersAction parameters = getAction(ParametersAction.class); + if (parameters != null) + parameters.buildEnvVars(this,env); + + if(buildEnvironments!=null) for (BuildWrapper.Environment e : buildEnvironments) e.buildEnvVars(env); - } + return env; } diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 44abfb2a954853cdbff368f03574f2d9a50973c9..5d240c0ac3f7863fcefba116b3fb1be9024fae9c 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -367,7 +367,7 @@ public abstract class AbstractProject

,R extends A */ public boolean scheduleBuild() { if(isDisabled()) return false; - return Hudson.getInstance().getQueue().add(this); + return Hudson.getInstance().getQueue().add(this, getQuietPeriod()); } /** @@ -845,6 +845,10 @@ public abstract class AbstractProject

,R extends A protected HistoryWidget createHistoryWidget() { return new BuildHistoryWidget(this,getBuilds(),HISTORY_ADAPTER); } + + public boolean isParameterized() { + return getProperty(ParametersDefinitionProperty.class) != null; + } // // @@ -857,6 +861,13 @@ public abstract class AbstractProject

,R extends A public void doBuild( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); + // if a build is parameterized, let that take over + ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class); + if (pp != null) { + pp._doBuild(req,rsp); + return; + } + String delay = req.getParameter("delay"); if (delay!=null) { if (!isDisabled()) { diff --git a/core/src/main/java/hudson/model/Build.java b/core/src/main/java/hudson/model/Build.java index 5fb180c7905ff16ae9bb236eb03193b95af0dc9f..65e00b452fcede81658c617a4f313e4082c7a2f3 100644 --- a/core/src/main/java/hudson/model/Build.java +++ b/core/src/main/java/hudson/model/Build.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; -import java.util.Map; import java.util.Collection; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; @@ -94,14 +93,19 @@ public abstract class Build

,B extends Build> buildEnvironments = new ArrayList(); try { - for( BuildWrapper w : project.getBuildWrappers().values() ) { + List wrappers = new ArrayList(project.getBuildWrappers().values()); + + ParametersAction parameters = getAction(ParametersAction.class); + if (parameters != null) + parameters.createBuildWrappers(Build.this,wrappers); + + for( BuildWrapper w : wrappers ) { Environment e = w.setUp((AbstractBuild)Build.this, launcher, listener); if(e==null) return Result.FAILURE; buildEnvironments.add(e); } - if(!build(listener,project.getBuilders())) return Result.FAILURE; } finally { diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index 2577e1927a17ac322551d5c9ef08d215b788ff4c..80c410a4c642fca34638929b62e6fde92910fefe 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -56,7 +56,7 @@ public class Executor extends Thread implements ModelObject { try { finishTime = System.currentTimeMillis(); while(true) { - if(Hudson.getInstance().isTerminating()) + if(Hudson.getInstance() == null || Hudson.getInstance().isTerminating()) return; synchronized(owner) { diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index adccb05a8dce3266249af5419a388fa59f25894e..034008f97bc40b5c826f60438e1d3b0a1a0e8ddd 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -76,6 +76,7 @@ import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.ui.AbstractProcessingFilter; import static org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY; +import org.apache.commons.logging.LogFactory; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; @@ -1424,6 +1425,7 @@ public final class Hudson extends View implements ItemGroup, Node, } ExternalJob.reloadThread.interrupt(); Trigger.timer.cancel(); + Trigger.timer = null; if(tcpSlaveAgentListener!=null) tcpSlaveAgentListener.shutdown(); @@ -1436,6 +1438,8 @@ public final class Hudson extends View implements ItemGroup, Node, getQueue().save(); threadPoolForLoad.shutdown(); + + LogFactory.releaseAll(); } public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { diff --git a/core/src/main/java/hudson/model/JobParameterDefinition.java b/core/src/main/java/hudson/model/JobParameterDefinition.java new file mode 100644 index 0000000000000000000000000000000000000000..42dea3c2abaae0b92e8fc794c036920fa4961a4b --- /dev/null +++ b/core/src/main/java/hudson/model/JobParameterDefinition.java @@ -0,0 +1,45 @@ +package hudson.model; + +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; + +public class JobParameterDefinition extends ParameterDefinition { + + @DataBoundConstructor + public JobParameterDefinition(String name) { + super(name); + } + + @Override + public ParameterDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final ParameterDescriptor DESCRIPTOR = new DescriptorImpl(); + + public static class DescriptorImpl extends ParameterDescriptor { + + protected DescriptorImpl() { + super(JobParameterDefinition.class); + } + + @Override + public String getDisplayName() { + return "Project Parameter"; + } + + @Override + public ParameterDefinition newInstance(StaplerRequest req, JSONObject formData) throws FormException { + return req.bindJSON(JobParameterDefinition.class, formData); + } + + } + + @Override + public ParameterValue createValue(StaplerRequest req, JSONObject jo) { + return req.bindJSON(JobParameterValue.class, jo); + } + +} diff --git a/core/src/main/java/hudson/model/JobParameterValue.java b/core/src/main/java/hudson/model/JobParameterValue.java new file mode 100644 index 0000000000000000000000000000000000000000..32e3b09e69710c7feef7d23899da28aad1a8a754 --- /dev/null +++ b/core/src/main/java/hudson/model/JobParameterValue.java @@ -0,0 +1,24 @@ +package hudson.model; + +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.Map; + +public class JobParameterValue extends ParameterValue { + public final Job job; + + @DataBoundConstructor + public JobParameterValue(String name, Job job) { + super(name); + this.job = job; + } + + /** + * Exposes the name/value as an environment variable. + */ + @Override + public void buildEnvVars(AbstractBuild build, Map env) { + // TODO: check with Tom if this is really what he had in mind + env.put(name.toUpperCase(),job.toString()); + } +} diff --git a/core/src/main/java/hudson/model/JobProperty.java b/core/src/main/java/hudson/model/JobProperty.java index ac85bb2e13523d133ebaee8ddf984257cb1ee4cb..378712b06af96cee54e4e8f5c678059726d05b08 100644 --- a/core/src/main/java/hudson/model/JobProperty.java +++ b/core/src/main/java/hudson/model/JobProperty.java @@ -61,6 +61,10 @@ public abstract class JobProperty> implements Describable + * Returning non-null from this method allows a job property to add an item + * to the left navigation bar in the job page. + * * @param job * Always the same as {@link #owner} but passed in anyway for backward compatibility (I guess.) * You really need not use this value at all. diff --git a/core/src/main/java/hudson/model/Jobs.java b/core/src/main/java/hudson/model/Jobs.java index 9d52a37b72dd8ee6bd0fc83d81c85823a48d7712..c43b299a3fedbcc1cd2ec34add4836d344607a9f 100644 --- a/core/src/main/java/hudson/model/Jobs.java +++ b/core/src/main/java/hudson/model/Jobs.java @@ -17,5 +17,6 @@ public class Jobs { * @see JobPropertyDescriptor#getPropertyDescriptors(Class) */ public static final List PROPERTIES = Descriptor.toList( + ParametersDefinitionProperty.DESCRIPTOR ); } diff --git a/core/src/main/java/hudson/model/ParameterDefinition.java b/core/src/main/java/hudson/model/ParameterDefinition.java new file mode 100644 index 0000000000000000000000000000000000000000..84c1bbf4f16b66010b5904f44aad100b3a5eff0d --- /dev/null +++ b/core/src/main/java/hudson/model/ParameterDefinition.java @@ -0,0 +1,108 @@ +package hudson.model; + +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.StaplerRequest; + +import hudson.util.DescriptorList; +import hudson.ExtensionPoint; + +/** + * Defines a parameter for a build. + * + *

+ * In Hudson, a user can configure a job to require parameters for a build. + * For example, imagine a test job that takes the bits to be tested as a parameter. + * + *

+ * The actual meaning and the purpose of parameters are entirely up to users, so + * what the concrete parameter implmentation is pluggable. Write subclasses + * in a plugin and hook it up to {@link #LIST} to register it. + * + *

+ * Three classes are used to model build parameters. First is the + * {@link ParameterDescriptor}, which tells Hudson what kind of implementations are + * available. From {@link ParameterDescriptor#newInstance(StaplerRequest, JSONObject)}, + * Hudson creates {@link ParameterDefinition}s based on the job configuration. + * For example, if the user defines two string parameters "database-type" and + * "appserver-type", we'll get two {@link StringParameterDefinition} instances + * with their respective names. + * + *

+ * When a job is configured with {@link ParameterDefinition} (or more precisely, + * {@link ParametersDefinitionProperty}, which in turns retains {@link ParameterDefinition}s), + * user would have to enter the values for the defined build parameters. + * The {@link #createValue(StaplerRequest, JSONObject)} method is used to convert this + * form submission into {@link ParameterValue} objects, which are then accessible + * during a build. + * + * + * + *

Persistence

+ *

+ * Instances of {@link ParameterDefinition}s are persisted into job config.xml + * through XStream. + * + * + *

Assocaited Views

+ *

config.jelly

+ *

+ * {@link ParameterDefinition} class uses config.jelly to provide contribute a form + * fragment in the job configuration screen. Values entered there is fed back to + * {@link ParameterDescriptor#newInstance(StaplerRequest, JSONObject)} to create {@link ParameterDefinition}s. + * + *

index.jelly

+ * The index.jelly view contributes a form fragment in the page where the user + * enters actual values of parameters for a build. The result of this form submission + * is then fed to {@link ParameterDefinition#createValue(StaplerRequest, JSONObject)} to + * create {@link ParameterValue}s. + * + * TODO: what Jelly pages does this object need for rendering UI? + * TODO: {@link ParameterValue} needs to have some mechanism to expose values to the build + * @see StringParameterDefinition + */ +public abstract class ParameterDefinition implements + Describable, ExtensionPoint { + + private final String name; + + public String getName() { + return name; + } + + public ParameterDefinition(String name) { + super(); + this.name = name; + } + + /** + * {@inheritDoc} + */ + public abstract ParameterDescriptor getDescriptor(); + + public abstract ParameterValue createValue(StaplerRequest req, JSONObject jo); + + /** + * A list of available parameter definition types + */ + public static final DescriptorList LIST = new DescriptorList(); + + public abstract static class ParameterDescriptor extends + Descriptor { + + protected ParameterDescriptor(Class klazz) { + super(klazz); + } + + public String getValuePage() { + return getViewPage(clazz, "index.jelly"); + } + + @Override + public String getDisplayName() { + return "Parameter"; + } + + } + +} diff --git a/core/src/main/java/hudson/model/ParameterValue.java b/core/src/main/java/hudson/model/ParameterValue.java new file mode 100644 index 0000000000000000000000000000000000000000..a87c76e47abdef407fd87a48a2a9ec1ee8f967fc --- /dev/null +++ b/core/src/main/java/hudson/model/ParameterValue.java @@ -0,0 +1,116 @@ +package hudson.model; + +import hudson.tasks.BuildWrapper; +import hudson.tasks.Builder; +import hudson.util.VariableResolver; + +import java.util.Map; + +import org.kohsuke.stapler.StaplerRequest; +import net.sf.json.JSONObject; + +/** + * A value for a parameter in a build. + * + * Created by {@link ParameterDefinition#createValue(StaplerRequest, JSONObject)} for + * a particular build (although this 'owner' build object is passed in for every method + * call as a parameter so that the parameter won't have to persist it.) + * + *

Persistence

+ *

+ * Instances of {@link ParameterValue}s are persisted into build's build.xml + * through XStream (via {@link ParametersAction}), so instances need to be persistable. + * + *

Assocaited Views

+ *

value.jelly

+ * The value.jelly view contributes a UI fragment to display the parameter + * values used for a build. + * + *

Notes

+ *
    + *
  1. {@link ParameterValue} is used to record values of the past build, but + * {@link ParameterDefinition} used back then might be gone already, or represent + * a different parameter now. So don't try to use the name to infer + * {@link ParameterDefinition} is. + *
+ * @see ParameterDefinition + */ +public abstract class ParameterValue { + protected final String name; + + protected ParameterValue(String name) { + this.name = name; + } + + /** + * Name of the parameter. + * + * This uniquely distinguishes {@link ParameterValue} among other parameters + * for the same build. This must be the same as {@link ParameterDefinition#getName()}. + */ + public final String getName() { + return name; + } + + /** + * Adds environmental variables for the builds to the given map. + * + *

+ * This provides a means for a parameter to pass the parameter + * values to the build to be performed. + * + *

+ * When this method is invoked, the map already contains the + * current "planned export" list. The implementation is + * expected to add more values to this map (or do nothing) + * + *

+ * Environment variables should be by convention all upper case. + * (This is so that a Windows/Unix heterogenous environment + * won't get inconsistent result depending on which platform to + * execute.) + * + * @param env + * never null. + * @param build + * The build for which this parameter is being used. Never null. + */ + public void buildEnvVars(AbstractBuild build, Map env) { + // no-op by default + } + + /** + * Called at the beginning of a build to let a {@link ParameterValue} + * contributes a {@link BuildWrapper} to the build. + * + *

+ * This provides a means for a parameter to perform more extensive + * set up / tear down during a build. + * + * @param build + * The build for which this parameter is being used. Never null. + * @return + * null if the parameter has no {@link BuildWrapper} to contribute to. + */ + public BuildWrapper createBuildWrapper(AbstractBuild build) { + return null; + } + + /** + * Returns a {@link VariableResolver} so that other components like {@link Builder}s + * can perform variable substitution to reflect parameter values into the build process. + * + * createVariableResolver(AbstractBuild build) { + return VariableResolver.NONE; + } +} diff --git a/core/src/main/java/hudson/model/ParameterizedProjectTask.java b/core/src/main/java/hudson/model/ParameterizedProjectTask.java new file mode 100644 index 0000000000000000000000000000000000000000..a9c2bd504bf9f3859d8c06068fa2c541b2e02442 --- /dev/null +++ b/core/src/main/java/hudson/model/ParameterizedProjectTask.java @@ -0,0 +1,63 @@ +package hudson.model; + +import hudson.model.Queue.Executable; +import hudson.util.QueueTaskFilter; + +import java.io.IOException; +import java.util.List; + +/** + * A task representing a project that should be built with a certain set of + * parameter values + */ +public class ParameterizedProjectTask extends QueueTaskFilter { + + private final AbstractProject project; + private final List parameters; + + public ParameterizedProjectTask(AbstractProject project, + List parameters) { + super(project); + this.project = project; + this.parameters = parameters; + } + + @Override + public Executable createExecutable() throws IOException { + AbstractBuild build = project.createExecutable(); + build.addAction(new ParametersAction(parameters, build)); + + return build; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((parameters == null) ? 0 : parameters.hashCode()); + result = prime * result + ((project == null) ? 0 : project.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ParameterizedProjectTask other = (ParameterizedProjectTask) obj; + if (parameters == null) { + if (other.parameters != null) + return false; + } else if (!parameters.equals(other.parameters)) { + return false; + } + if (project != other.project) { + return false; + } + return true; + } +} diff --git a/core/src/main/java/hudson/model/ParametersAction.java b/core/src/main/java/hudson/model/ParametersAction.java new file mode 100644 index 0000000000000000000000000000000000000000..6600e6e0c3034e76cd8a24b2d41db0629751f540 --- /dev/null +++ b/core/src/main/java/hudson/model/ParametersAction.java @@ -0,0 +1,86 @@ +package hudson.model; + +import hudson.Util; +import hudson.tasks.BuildWrapper; +import hudson.util.VariableResolver; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Records the parameter values used for a build. + * + *

+ * This object is associated with the build record so that we remember what parameters + * were used for what build. + */ +public class ParametersAction implements Action, Iterable { + + private final List parameters; + private final AbstractBuild build; + + public ParametersAction(List parameters, AbstractBuild build) { + this.parameters = parameters; + this.build = build; + } + + public void createBuildWrappers(AbstractBuild build, Collection result) { + for (ParameterValue p : parameters) { + BuildWrapper w = p.createBuildWrapper(build); + if(w!=null) result.add(w); + } + } + + public void buildEnvVars(AbstractBuild build, Map env) { + for (ParameterValue p : parameters) + p.buildEnvVars(build,env); + } + + /** + * Performs a variable subsitution to the given text and return it. + */ + public String substitute(AbstractBuild build, String text) { + return Util.replaceMacro(text,createVariableResolver(build)); + } + + /** + * Creates an {@link VariableResolver} that aggregates all the parameters. + */ + public VariableResolver createVariableResolver(AbstractBuild build) { + VariableResolver[] resolvers = new VariableResolver[parameters.size()]; + int i=0; + for (ParameterValue p : parameters) + resolvers[i++] = p.createVariableResolver(build); + + return new VariableResolver.Union(resolvers); + } + + public AbstractBuild getBuild() { + return build; + } + + public Iterator iterator() { + return parameters.iterator(); + } + + public List getParameters() { + return parameters; + } + + @Override + public String getDisplayName() { + return "Parameters"; + } + + @Override + public String getIconFileName() { + return "document-properties.gif"; + } + + @Override + public String getUrlName() { + return "parameters"; + } +} diff --git a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java new file mode 100644 index 0000000000000000000000000000000000000000..e685f5505d497abb7ea4743edb6af59b881a8e61 --- /dev/null +++ b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java @@ -0,0 +1,154 @@ +package hudson.model; + +import hudson.StructuredForm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; + +/** + * Keeps a list of the parameters defined for a project. + * + *

+ * This class also implements {@link Action} so that index.jelly provides + * a form to enter build parameters. + */ +public class ParametersDefinitionProperty extends JobProperty> + implements Action { + + private final List parameterDefinitions; + + public ParametersDefinitionProperty(List parameterDefinitions) { + this.parameterDefinitions = parameterDefinitions; + } + + public AbstractProject getOwner() { + return owner; + } + + public List getParameterDefinitions() { + return parameterDefinitions; + } + + @Override + public Action getJobAction(AbstractProject job) { + return this; + } + + public AbstractProject getProject() { + return (AbstractProject) owner; + } + + /** + * Interprets the form submission and schedules a build for a parameterized job. + * + *

+ * This method is supposed to be invoked from {@link AbstractProject#doBuild(StaplerRequest, StaplerResponse)}. + */ + public void _doBuild(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + if(!req.getMethod().equals("POST")) { + // show the parameter entry form. + req.getView(this,"index.jelly").forward(req,rsp); + return; + } + + List values = new ArrayList(); + + JSONObject formData = StructuredForm.get(req); + JSONArray a = JSONArray.fromObject(formData.get("parameter")); + + for (Object o : a) { + JSONObject jo = (JSONObject) o; + String name = jo.getString("name"); + + ParameterDefinition d = getParameterDefinition(name); + if(d==null) + throw new IllegalArgumentException("No such parameter definition: " + name); + values.add(d.createValue(req, jo)); + } + + Hudson.getInstance().getQueue().add( + new ParameterizedProjectTask(owner, values), 0); + + // send the user back to the job top page. + rsp.sendRedirect("."); + } + + /** + * Gets the {@link ParameterDefinition} of the given name, if any. + */ + public ParameterDefinition getParameterDefinition(String name) { + for (ParameterDefinition pd : parameterDefinitions) + if (pd.getName().equals(name)) + return pd; + return null; + } + + @Override + public JobPropertyDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final JobPropertyDescriptor DESCRIPTOR = new DescriptorImpl(); + + public static class DescriptorImpl extends JobPropertyDescriptor { + + protected DescriptorImpl() { + super(ParametersDefinitionProperty.class); + } + + @Override + public boolean isApplicable(Class jobType) { + return AbstractProject.class.isAssignableFrom(jobType); + } + + @Override + public JobProperty newInstance(StaplerRequest req, + JSONObject formData) throws FormException { + if (formData.isNullObject()) { + return null; + } + + List parameterDefinitions = Descriptor.newInstancesFromHeteroList( + req, formData, "parameter", ParameterDefinition.LIST); + if(parameterDefinitions.isEmpty()) + return null; + + return new ParametersDefinitionProperty(parameterDefinitions); + } + + @Override + public String getDisplayName() { + return "Parameters"; + } + + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getUrlName() { + return "parameters"; + } + + + static { + ParameterDefinition.LIST.add(StringParameterDefinition.DESCRIPTOR); + } +} diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index f7e90c2b0cbdde9569e2388f80ad726dcaeb01d4..0b52d2c9a1c03e6cfdd50b33b86d06a7faf9601b 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -1,28 +1,42 @@ package hudson.model; import hudson.Util; +import hudson.XmlFile; import hudson.model.Node.Mode; import hudson.triggers.SafeTimerTask; import hudson.triggers.Trigger; import hudson.util.OneShotEvent; -import org.acegisecurity.AccessDeniedException; -import org.kohsuke.stapler.export.Exported; -import org.kohsuke.stapler.export.ExportedBean; +import hudson.util.XStream2; -import javax.management.timer.Timer; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.Map.Entry; -import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; +import javax.management.timer.Timer; + +import org.acegisecurity.AccessDeniedException; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter; + /** * Build queue. * @@ -130,7 +144,52 @@ public class Queue extends ResourceController { */ private final Map parked = new HashMap(); + private final XStream xstream = new XStream2(); + public Queue() { + xstream.registerConverter(new AbstractSingleValueConverter() { + + @Override + @SuppressWarnings("unchecked") + public boolean canConvert(Class klazz) { + return TopLevelItem.class.isAssignableFrom(klazz); + } + + @Override + public Object fromString(String string) { + return Hudson.getInstance().getItem(string); + } + + @Override + public String toString(Object item) { + return ((TopLevelItem) item).getName(); + } + }); + xstream.registerConverter(new AbstractSingleValueConverter() { + + @SuppressWarnings("unchecked") + @Override + public boolean canConvert(Class klazz) { + return Run.class.isAssignableFrom(klazz); + } + + @Override + public Object fromString(String string) { + String[] split = string.split("#"); + String projectName = split[0]; + int buildNumber = Integer.parseInt(split[1]); + Job job = (Job) Hudson.getInstance().getItem(projectName); + Run run = job.getBuildByNumber(buildNumber); + return run; + } + + @Override + public String toString(Object object) { + Run run = (Run) object; + return run.getParent().getName() + "#" + run.getNumber(); + } + }); + // if all the executors are busy doing something, then the queue won't be maintained in // timely fashion, so use another thread to make sure it happens. new MaintainTask(this); @@ -140,22 +199,32 @@ public class Queue extends ResourceController { * Loads the queue contents that was {@link #save() saved}. */ public synchronized void load() { - // write out the contents of the queue try { + // first try the old format File queueFile = getQueueFile(); - if (!queueFile.exists()) - return; - - BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(queueFile))); - String line; - while ((line = in.readLine()) != null) { - AbstractProject j = Hudson.getInstance().getItemByFullName(line, AbstractProject.class); - if (j != null) - j.scheduleBuild(); - } - in.close(); - // discard the queue file now that we are done - queueFile.delete(); + if (queueFile.exists()) { + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(queueFile))); + String line; + while ((line = in.readLine()) != null) { + AbstractProject j = Hudson.getInstance().getItemByFullName(line, AbstractProject.class); + if (j != null) + j.scheduleBuild(); + } + in.close(); + // discard the queue file now that we are done + queueFile.delete(); + + return; + } else { + queueFile = getXMLQueueFile(); + if (queueFile.exists()) { + List tasks = (List) new XmlFile(xstream, queueFile).read(); + for (Task task: tasks) { + add(task, 0); + } + queueFile.delete(); + } + } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to load the queue file " + getQueueFile(), e); } @@ -165,21 +234,26 @@ public class Queue extends ResourceController { * Persists the queue contents to the disk. */ public synchronized void save() { - // write out the contents of the queue + // write out the tasks on the queue + ArrayList tasks = new ArrayList(); + for (Item item: getItems()) { + tasks.add(item.task); + } + try { - PrintWriter w = new PrintWriter(new FileOutputStream( - getQueueFile())); - for (Item i : getItems()) - w.println(i.task.getName()); - w.close(); + new XmlFile(xstream, getXMLQueueFile()).write(tasks); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to write out the queue file " + getQueueFile(), e); } } private File getQueueFile() { - return new File(Hudson.getInstance().getRootDir(), "queue.txt"); - } + return new File(Hudson.getInstance().getRootDir(), "queue.txt"); + } + + private File getXMLQueueFile() { + return new File(Hudson.getInstance().getRootDir(), "queue.xml"); + } /** * Schedule a new build for this project. @@ -245,10 +319,10 @@ public class Queue extends ResourceController { * @return true if the project was indeed in the queue and was removed. * false if this was no-op. */ - public synchronized boolean cancel(AbstractProject p) { + public synchronized boolean cancel(Task p) { LOGGER.fine("Cancelling " + p.getName()); - for (Iterator itr = waitingList.iterator(); itr.hasNext();) { - Item item = (Item) itr.next(); + for (Iterator itr = waitingList.iterator(); itr.hasNext();) { + Item item = itr.next(); if (item.task == p) { itr.remove(); return true; @@ -319,10 +393,10 @@ public class Queue extends ResourceController { * Left for backward compatibility. * * @see #getItem(Task) - */ public synchronized Item getItem(AbstractProject p) { return getItem((Task) p); } + */ /** * Returns true if this queue contains the said project. diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 733f2039cd536c721f7d55a2e0810d1b87971a65..8bbe17d986f52c511a2d232467160ac7fd9a6168 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -781,6 +781,8 @@ public abstract class Run ,RunT extends Run> extends AbstractMap imp builds.put( b.getNumber(), b ); } catch (IOException e) { e.printStackTrace(); + } catch (InstantiationError e) { + e.printStackTrace(); } } } diff --git a/core/src/main/java/hudson/model/RunParameterDefinition.java b/core/src/main/java/hudson/model/RunParameterDefinition.java new file mode 100644 index 0000000000000000000000000000000000000000..fcd1ce702331d246e99a1a6d9a8ae184a7d89a1b --- /dev/null +++ b/core/src/main/java/hudson/model/RunParameterDefinition.java @@ -0,0 +1,45 @@ +package hudson.model; + +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; + +public class RunParameterDefinition extends ParameterDefinition { + + @DataBoundConstructor + public RunParameterDefinition(String name) { + super(name); + } + + @Override + public ParameterDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final ParameterDescriptor DESCRIPTOR = new DescriptorImpl(); + + public static class DescriptorImpl extends ParameterDescriptor { + + protected DescriptorImpl() { + super(RunParameterDefinition.class); + } + + @Override + public String getDisplayName() { + return "Run Parameter"; + } + + @Override + public ParameterDefinition newInstance(StaplerRequest req, JSONObject formData) throws FormException { + return req.bindJSON(RunParameterDefinition.class, formData); + } + + } + + @Override + public ParameterValue createValue(StaplerRequest req, JSONObject jo) { + return req.bindJSON(RunParameterValue.class, jo); + } + +} diff --git a/core/src/main/java/hudson/model/RunParameterValue.java b/core/src/main/java/hudson/model/RunParameterValue.java new file mode 100644 index 0000000000000000000000000000000000000000..d278e8da63875a41c45ec3f383d2085d3ec22097 --- /dev/null +++ b/core/src/main/java/hudson/model/RunParameterValue.java @@ -0,0 +1,25 @@ +package hudson.model; + +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.Map; + +public class RunParameterValue extends ParameterValue { + + public final Run run; + + @DataBoundConstructor + public RunParameterValue(String name, Run run) { + super(name); + this.run = run; + } + + /** + * Exposes the name/value as an environment variable. + */ + @Override + public void buildEnvVars(AbstractBuild build, Map env) { + // TODO: check with Tom if this is really what he had in mind + env.put(name.toUpperCase(),run.toString()); + } +} diff --git a/core/src/main/java/hudson/model/StringParameterDefinition.java b/core/src/main/java/hudson/model/StringParameterDefinition.java new file mode 100644 index 0000000000000000000000000000000000000000..8738495440b3c6cd4435bceb3168dc66a505ccf9 --- /dev/null +++ b/core/src/main/java/hudson/model/StringParameterDefinition.java @@ -0,0 +1,71 @@ +package hudson.model; + +import net.sf.json.JSONObject; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; + +/** + * Parameter whose value is a string value. + */ +public class StringParameterDefinition extends ParameterDefinition { + + private boolean optional; + private String defaultValue; + + @DataBoundConstructor + public StringParameterDefinition(String name, String defaultValue, + boolean optional) { + super(name); + this.defaultValue = defaultValue; + this.optional = optional; + } + + @Override + public ParameterDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public boolean isOptional() { + return optional; + } + + public void setOptional(boolean optional) { + this.optional = optional; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public static final ParameterDescriptor DESCRIPTOR = new DescriptorImpl(); + + public static class DescriptorImpl extends ParameterDescriptor { + + protected DescriptorImpl() { + super(StringParameterDefinition.class); + } + + @Override + public String getDisplayName() { + return "String Parameter"; + } + + @Override + public ParameterDefinition newInstance(StaplerRequest req, + JSONObject formData) throws FormException { + return req.bindJSON(StringParameterDefinition.class, formData); + } + + } + + @Override + public ParameterValue createValue(StaplerRequest req, JSONObject jo) { + return req.bindJSON(StringParameterValue.class, jo); + } + +} diff --git a/core/src/main/java/hudson/model/StringParameterValue.java b/core/src/main/java/hudson/model/StringParameterValue.java new file mode 100644 index 0000000000000000000000000000000000000000..04c25caa7665e80a9c5e241dda22d3a2ca6e7e85 --- /dev/null +++ b/core/src/main/java/hudson/model/StringParameterValue.java @@ -0,0 +1,26 @@ +package hudson.model; + +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.Map; + +/** + * {@link ParameterValue} created from {@link StringParameterDefinition}. + */ +public class StringParameterValue extends ParameterValue { + public final String value; + + @DataBoundConstructor + public StringParameterValue(String name, String value) { + super(name); + this.value = value; + } + + /** + * Exposes the name/value as an environment variable. + */ + @Override + public void buildEnvVars(AbstractBuild build, Map env) { + env.put(name.toUpperCase(),value); + } +} diff --git a/core/src/main/java/hudson/model/listeners/RunListener.java b/core/src/main/java/hudson/model/listeners/RunListener.java index 12d6fc883243d83e39af5a4e30dcf5005913fb5a..ca3071ad8a5b76272c9297dc1db7402adc1c6089 100644 --- a/core/src/main/java/hudson/model/listeners/RunListener.java +++ b/core/src/main/java/hudson/model/listeners/RunListener.java @@ -36,6 +36,19 @@ public abstract class RunListener implements ExtensionPoint { * the build is considered completed, so its status cannot be changed anymore. */ public void onCompleted(R r, TaskListener listener) {} + + /** + * Called when a build is started (i.e. it was in the queue, and will now start running + * on an executor) + * + * @param r + * The started build. + * @param listener + * The listener for this build. This can be used to produce log messages, for example, + * which becomes a part of the "console output" of this build. But when this method runs, + * the build is considered completed, so its status cannot be changed anymore. + */ + public void onStarted(R r, TaskListener listener) {} /** * Registers this object as an active listener so that it can start getting @@ -66,4 +79,14 @@ public abstract class RunListener implements ExtensionPoint { l.onCompleted(r,listener); } } + + /** + * Fires the {@link #onStarted} event. + */ + public static void fireStarted(Run r, TaskListener listener) { + for (RunListener l : LISTENERS) { + if(l.targetType.isInstance(r)) + l.onStarted(r,listener); + } + } } diff --git a/core/src/main/java/hudson/security/AuthorizationMatrixProperty.java b/core/src/main/java/hudson/security/AuthorizationMatrixProperty.java new file mode 100644 index 0000000000000000000000000000000000000000..a65bd2fab81e94f7944d2f1f9c031b6a23e4d796 --- /dev/null +++ b/core/src/main/java/hudson/security/AuthorizationMatrixProperty.java @@ -0,0 +1,238 @@ +package hudson.security; + +import hudson.model.Item; +import hudson.model.Job; +import hudson.model.JobProperty; +import hudson.model.JobPropertyDescriptor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import net.sf.json.JSONObject; + +import org.acegisecurity.Authentication; +import org.acegisecurity.acls.sid.GrantedAuthoritySid; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.acls.sid.Sid; +import org.kohsuke.stapler.StaplerRequest; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +public class AuthorizationMatrixProperty extends JobProperty> { + + public static final JobPropertyDescriptor DESCRIPTOR = new DescriptorImpl(); + + private transient ACL acl = new AclImpl(); + + private boolean useProjectSecurity; + + public boolean isUseProjectSecurity() { + return useProjectSecurity; + } + + public void setUseProjectSecurity(boolean useProjectSecurity) { + this.useProjectSecurity = useProjectSecurity; + } + + /** + * List up all permissions that are granted. + * + * Strings are either the granted authority or the principal, which is not + * distinguished. + */ + private final Map> grantedPermissions = new HashMap>(); + + private Set sids = new HashSet(); + + public Set getGroups() { + return sids; + } + + /** + * Returns all SIDs configured in this matrix, minus "anonymous" + * + * @return Always non-null. + */ + public List getAllSIDs() { + Set r = new HashSet(); + for (Set set : grantedPermissions.values()) + r.addAll(set); + r.remove("anonymous"); + + String[] data = r.toArray(new String[r.size()]); + Arrays.sort(data); + return Arrays.asList(data); + } + + /** + * Adds to {@link #grantedPermissions}. Use of this method should be limited + * during construction, as this object itself is considered immutable once + * populated. + */ + protected void add(Permission p, String sid) { + Set set = grantedPermissions.get(p); + if (set == null) + grantedPermissions.put(p, set = new HashSet()); + set.add(sid); + sids.add(sid); + } + + @Override + public JobPropertyDescriptor getDescriptor() { + return DESCRIPTOR; + } + + public static class DescriptorImpl extends JobPropertyDescriptor { + + @Override + public JobProperty newInstance(StaplerRequest req, + JSONObject formData) throws FormException { + boolean useProjectSecurity = formData.has("useProjectSecurity"); + AuthorizationMatrixProperty amp = new AuthorizationMatrixProperty(); + amp.setUseProjectSecurity(useProjectSecurity); + for (Map.Entry r : (Set>) formData + .getJSONObject("data").entrySet()) { + String sid = r.getKey(); + if (r.getValue() instanceof JSONObject) { + for (Map.Entry e : (Set>) ((JSONObject) r + .getValue()).entrySet()) { + if (e.getValue()) { + Permission p = Permission.fromId(e.getKey()); + amp.add(p, sid); + } + } + } + } + return amp; + } + + protected DescriptorImpl() { + super(AuthorizationMatrixProperty.class); + } + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + @Override + public String getDisplayName() { + return "Authorization Matrix"; + } + + public List getAllGroups() { + return Collections.singletonList(PermissionGroup.get(Item.class)); + } + + } + + private final class AclImpl extends SidACL { + protected Boolean hasPermission(Sid sid, Permission p) { + String s = toString(sid); + for (; p != null; p = p.impliedBy) { + Set set = grantedPermissions.get(p); + if (set != null && set.contains(s)) + return true; + } + return false; + } + + protected Boolean _hasPermission(Authentication a, Permission permission) { + Boolean b = super._hasPermission(a, permission); + // permissions granted to anonymous users are granted to everyone + if (b == null) + b = hasPermission(ANONYMOUS, permission); + return b; + } + + private String toString(Sid p) { + if (p instanceof GrantedAuthoritySid) + return ((GrantedAuthoritySid) p).getGrantedAuthority(); + if (p instanceof PrincipalSid) + return ((PrincipalSid) p).getPrincipal(); + // hmm... + return p.toString(); + } + } + + private Object readResolve() { + acl = new AclImpl(); + return this; + } + + public ACL getACL() { + return acl; + } + + /** + * Checks if the given SID has the given permission. + */ + public boolean hasPermission(String sid, Permission p) { + for (; p != null; p = p.impliedBy) { + Set set = grantedPermissions.get(p); + if (set != null && set.contains(sid)) + return true; + } + return false; + } + + /** + * Works like {@link #add(Permission, String)} but takes both parameters + * from a single string of the form PERMISSIONID:sid + */ + private void add(String shortForm) { + int idx = shortForm.indexOf(':'); + add(Permission.fromId(shortForm.substring(0, idx)), shortForm + .substring(idx + 1)); + } + + /** + * Persist {@link ProjectMatrixAuthorizationStrategy} as a list of IDs that + * represent {@link ProjectMatrixAuthorizationStrategy#grantedPermissions}. + */ + public static final class ConverterImpl implements Converter { + public boolean canConvert(Class type) { + return type == AuthorizationMatrixProperty.class; + } + + public void marshal(Object source, HierarchicalStreamWriter writer, + MarshallingContext context) { + AuthorizationMatrixProperty amp = (AuthorizationMatrixProperty) source; + + for (Entry> e : amp.grantedPermissions + .entrySet()) { + String p = e.getKey().getId(); + for (String sid : e.getValue()) { + writer.startNode("permission"); + context.convertAnother(p + ':' + sid); + writer.endNode(); + } + } + + } + + public Object unmarshal(HierarchicalStreamReader reader, + final UnmarshallingContext context) { + AuthorizationMatrixProperty as = new AuthorizationMatrixProperty(); + + while (reader.hasMoreChildren()) { + reader.moveDown(); + String id = (String) context.convertAnother(as, String.class); + as.add(id); + reader.moveUp(); + } + + return as; + } + } +} diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java index 5788511e0eaab97556735c09bf28f3e74102c2a4..a3f8b747ef42de00b5f6d64985154c42ae09f00e 100644 --- a/core/src/main/java/hudson/security/AuthorizationStrategy.java +++ b/core/src/main/java/hudson/security/AuthorizationStrategy.java @@ -173,7 +173,8 @@ public abstract class AuthorizationStrategy implements Describable project) { - SparseACL acl = new SparseACL(ROOT_ACL); - acl.add(new GrantedAuthoritySid(createItemAuthority(project)), Permission.FULL_CONTROL, true); - return acl; - } - - @Override - public ACL getACL(Computer computer) { - SparseACL acl = new SparseACL(ROOT_ACL); - acl.add(ACL.ANONYMOUS, Hudson.ADMINISTER, false); - acl.add(ACL.EVERYONE, Hudson.ADMINISTER, true); - return acl; - } - - @Override - public ACL getACL(View view) { - SparseACL acl = new SparseACL(ROOT_ACL); - acl.add(ACL.ANONYMOUS, Permission.CREATE, false); - acl.add(ACL.EVERYONE, Permission.CREATE, true); - return acl; - } - - public List getGroups() { - return Collections.emptyList(); - } - - public static final SparseACL ROOT_ACL = new SparseACL(null); - - static { - ROOT_ACL.add(new GrantedAuthoritySid("admin"), Permission.FULL_CONTROL, true); - ROOT_ACL.add(ACL.ANONYMOUS,FormFieldValidator.CHECK,false); - ROOT_ACL.add(ACL.EVERYONE,FormFieldValidator.CHECK,true); - ROOT_ACL.add(ACL.ANONYMOUS, Permission.CREATE, false); - ROOT_ACL.add(ACL.EVERYONE, Permission.CREATE, true); - ROOT_ACL.add(ACL.EVERYONE,Permission.READ,true); - ROOT_ACL.add(ACL.EVERYONE,Permission.FULL_CONTROL,false); - } - - public GrantedAuthority createItemAuthority(Item item) { - return new GrantedAuthorityImpl(item.getFullName()); - } - - public Descriptor getDescriptor() { - return DESCRIPTOR; - } - - public static final Descriptor DESCRIPTOR = new Descriptor(ProjectBasedAuthorizationStrategy.class) { - public String getDisplayName() { - return "Project Based Access Control"; - } - - public AuthorizationStrategy newInstance(StaplerRequest req, JSONObject formData) throws FormException { - return new ProjectBasedAuthorizationStrategy(); - } - - public String getHelpFile() { - return "/help/security/full-control-once-logged-in.html"; - } - }; -} diff --git a/core/src/main/java/hudson/security/ProjectMatrixAuthorizationStrategy.java b/core/src/main/java/hudson/security/ProjectMatrixAuthorizationStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..0ae6fb8bf7cb14ccc3f4c7b7277f7a96054bc648 --- /dev/null +++ b/core/src/main/java/hudson/security/ProjectMatrixAuthorizationStrategy.java @@ -0,0 +1,230 @@ +package hudson.security; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +import hudson.model.AbstractProject; +import hudson.model.Descriptor; +import hudson.model.Job; +import hudson.model.JobProperty; +import hudson.model.Jobs; +import net.sf.json.JSONObject; +import org.acegisecurity.Authentication; +import org.acegisecurity.acls.sid.GrantedAuthoritySid; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.acls.sid.Sid; +import org.kohsuke.stapler.StaplerRequest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Role-based authorization via a matrix. + * + * @author Kohsuke Kawaguchi + */ +public class ProjectMatrixAuthorizationStrategy extends AuthorizationStrategy { + protected transient ACL acl = new AclImpl(); + + /** + * List up all permissions that are granted. + * + * Strings are either the granted authority or the principal, + * which is not distinguished. + */ + private final Map> grantedPermissions = new HashMap>(); + + private final Set sids = new HashSet(); + + /** + * Adds to {@link #grantedPermissions}. + * Use of this method should be limited during construction, + * as this object itself is considered immutable once populated. + */ + protected void add(Permission p, String sid) { + Set set = grantedPermissions.get(p); + if(set==null) + grantedPermissions.put(p,set = new HashSet()); + set.add(sid); + sids.add(sid); + } + + /** + * Works like {@link #add(Permission, String)} but takes both parameters + * from a single string of the form PERMISSIONID:sid + */ + private void add(String shortForm) { + int idx = shortForm.indexOf(':'); + add(Permission.fromId(shortForm.substring(0,idx)),shortForm.substring(idx+1)); + } + + @Override + public ACL getRootACL() { + return acl; + } + + @Override + public ACL getACL(AbstractProject project) { + AuthorizationMatrixProperty amp = project.getProperty(AuthorizationMatrixProperty.class); + if (amp != null && amp.isUseProjectSecurity()) { + return amp.getACL(); + } else { + return getRootACL(); + } + } + + public Set getGroups() { + return sids; + } + + private Object readResolve() { + acl = new AclImpl(); + return this; + } + + /** + * Checks if the given SID has the given permission. + */ + public boolean hasPermission(String sid, Permission p) { + for(; p!=null; p=p.impliedBy) { + Set set = grantedPermissions.get(p); + if(set!=null && set.contains(sid)) + return true; + } + return false; + } + + /** + * Returns all SIDs configured in this matrix, minus "anonymous" + * + * @return + * Always non-null. + */ + public List getAllSIDs() { + Set r = new HashSet(); + for (Set set : grantedPermissions.values()) + r.addAll(set); + r.remove("anonymous"); + + String[] data = r.toArray(new String[r.size()]); + Arrays.sort(data); + return Arrays.asList(data); + } + + protected final class AclImpl extends SidACL { + protected Boolean hasPermission(Sid p, Permission permission) { + if(ProjectMatrixAuthorizationStrategy.this.hasPermission(toString(p),permission)) + return true; + return null; + } + + protected Boolean _hasPermission(Authentication a, Permission permission) { + Boolean b = super._hasPermission(a,permission); + // permissions granted to anonymous users are granted to everyone + if(b==null) b=hasPermission(ANONYMOUS,permission); + return b; + } + + private String toString(Sid p) { + if (p instanceof GrantedAuthoritySid) + return ((GrantedAuthoritySid) p).getGrantedAuthority(); + if (p instanceof PrincipalSid) + return ((PrincipalSid) p).getPrincipal(); + // hmm... + return p.toString(); + } + } + + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + public static final Descriptor DESCRIPTOR = new DescriptorImpl(); + + /** + * Persist {@link ProjectMatrixAuthorizationStrategy} as a list of IDs that + * represent {@link ProjectMatrixAuthorizationStrategy#grantedPermissions}. + */ + public static final class ConverterImpl implements Converter { + public boolean canConvert(Class type) { + return type== ProjectMatrixAuthorizationStrategy.class; + } + + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + ProjectMatrixAuthorizationStrategy strategy = (ProjectMatrixAuthorizationStrategy)source; + + for (Entry> e : strategy.grantedPermissions.entrySet()) { + String p = e.getKey().getId(); + for (String sid : e.getValue()) { + writer.startNode("permission"); + context.convertAnother(p+':'+sid); + writer.endNode(); + } + } + + } + + public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { + ProjectMatrixAuthorizationStrategy as = new ProjectMatrixAuthorizationStrategy(); + + while (reader.hasMoreChildren()) { + reader.moveDown(); + String id = (String)context.convertAnother(as,String.class); + as.add(id); + reader.moveUp(); + } + + return as; + } + } + + static { + LIST.add(DESCRIPTOR); + Jobs.PROPERTIES.add(AuthorizationMatrixProperty.DESCRIPTOR); + } + + public static final class DescriptorImpl extends Descriptor { + public DescriptorImpl() { + super(ProjectMatrixAuthorizationStrategy.class); + } + + public String getDisplayName() { + return "Project-based Matrix Authorization Strategy"; + } + + public AuthorizationStrategy newInstance(StaplerRequest req, JSONObject formData) throws FormException { + ProjectMatrixAuthorizationStrategy gmas = new ProjectMatrixAuthorizationStrategy(); + for(Map.Entry r : (Set>)formData.getJSONObject("data").entrySet()) { + String sid = r.getKey(); + for(Map.Entry e : (Set>)r.getValue().entrySet()) { + if(e.getValue()) { + Permission p = Permission.fromId(e.getKey()); + gmas.add(p,sid); + } + } + } + return gmas; + } + + public String getHelpFile() { + return "/help/security/global-matrix.html"; + } + + public List getAllGroups() { + List groups = new ArrayList(PermissionGroup.getAll()); + groups.remove(PermissionGroup.get(Permission.class)); + return groups; + } + } + +} + diff --git a/core/src/main/java/hudson/tasks/Ant.java b/core/src/main/java/hudson/tasks/Ant.java index 8f90b5f5e1b04667204a70bc929358c102b2bbbe..f792255d7edb8f608d63ab55f15070d7757f972f 100644 --- a/core/src/main/java/hudson/tasks/Ant.java +++ b/core/src/main/java/hudson/tasks/Ant.java @@ -10,6 +10,7 @@ import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Descriptor; +import hudson.model.ParametersAction; import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.util.ArgumentListBuilder; @@ -105,6 +106,11 @@ public class Ant extends Builder { public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { AbstractProject proj = build.getProject(); + String targets = this.targets; + ParametersAction parameters = build.getAction(ParametersAction.class); + if (parameters != null) + targets = parameters.substitute(build,targets); + ArgumentListBuilder args = new ArgumentListBuilder(); String execName; diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java index 8c403ef5671e3db4dece60e2dec4aaaf6919a2eb..b4bd568bd4f8f643ec376fdfaf4bcdc267e7088e 100644 --- a/core/src/main/java/hudson/tasks/Maven.java +++ b/core/src/main/java/hudson/tasks/Maven.java @@ -15,6 +15,7 @@ import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Descriptor; +import hudson.model.ParametersAction; import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.util.ArgumentListBuilder; @@ -125,6 +126,11 @@ public class Maven extends Builder { public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { AbstractProject proj = build.getProject(); + String targets = this.targets; + ParametersAction parameters = build.getAction(ParametersAction.class); + if (parameters != null) + targets = parameters.substitute(build,targets); + int startIndex = 0; int endIndex; do { diff --git a/core/src/main/java/hudson/triggers/Trigger.java b/core/src/main/java/hudson/triggers/Trigger.java index 5b03a62d99e2d3a28b9adf297e4eb2993ee18d5b..c43824752998b2402eea7d4730e396aa66b98a86 100644 --- a/core/src/main/java/hudson/triggers/Trigger.java +++ b/core/src/main/java/hudson/triggers/Trigger.java @@ -203,7 +203,7 @@ public abstract class Trigger implements Describable> * This timer is available for all the components inside Hudson to schedule * some work. */ - public static final Timer timer = new Timer("Hudson cron thread"); + public static Timer timer; public static void init() { long MIN = 1000*60; diff --git a/core/src/main/java/hudson/util/PluginServletFilter.java b/core/src/main/java/hudson/util/PluginServletFilter.java index 2ef98d6e9b2aac9b8b69487dc00da47c8b797137..2bb8c5e727c1867bc094be13489877c8ea5ee98d 100644 --- a/core/src/main/java/hudson/util/PluginServletFilter.java +++ b/core/src/main/java/hudson/util/PluginServletFilter.java @@ -16,14 +16,29 @@ import java.util.Collection; */ public class PluginServletFilter implements Filter { - public static CopyOnWriteList LIST = new CopyOnWriteList(); + private static CopyOnWriteList LIST = new CopyOnWriteList(); + + private static FilterConfig filterConfig; public PluginServletFilter() { } public void init(FilterConfig filterConfig) throws ServletException { - for (Filter f : LIST) - f.init(filterConfig); + this.filterConfig = filterConfig; + synchronized (LIST) { + for (Filter f : LIST) { + f.init(filterConfig); + } + } + } + + public static void addFilter(Filter filter) throws ServletException { + synchronized (LIST) { + if (filterConfig != null) { + filter.init(filterConfig); + } + LIST.add(filter); + } } public void doFilter(ServletRequest request, ServletResponse response, final FilterChain chain) throws IOException, ServletException { diff --git a/core/src/main/java/hudson/util/QueueTaskFilter.java b/core/src/main/java/hudson/util/QueueTaskFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..50a6d725001fc2b68730c6d73fb87a447a5e2b39 --- /dev/null +++ b/core/src/main/java/hudson/util/QueueTaskFilter.java @@ -0,0 +1,80 @@ +package hudson.util; + +import hudson.model.Queue; +import hudson.model.Label; +import hudson.model.Node; +import hudson.model.ResourceList; + +import java.io.IOException; + +/** + * Convenient base class for {@link Queue.Task} decorators. + * + * @author Kohsuke Kawaguchi + */ +public abstract class QueueTaskFilter implements Queue.Task { + /** + * Delegation target. + */ + protected volatile Queue.Task delegate; + + protected QueueTaskFilter(Queue.Task delegate) { + this.delegate = delegate; + } + + protected QueueTaskFilter() { + } + + public Label getAssignedLabel() { + return delegate.getAssignedLabel(); + } + + public Node getLastBuiltOn() { + return delegate.getLastBuiltOn(); + } + + public boolean isBuildBlocked() { + return delegate.isBuildBlocked(); + } + + public String getWhyBlocked() { + return delegate.getWhyBlocked(); + } + + public String getName() { + return delegate.getName(); + } + + public String getFullDisplayName() { + return delegate.getFullDisplayName(); + } + + public long getEstimatedDuration() { + return delegate.getEstimatedDuration(); + } + + public Queue.Executable createExecutable() throws IOException { + return delegate.createExecutable(); + } + + public void checkAbortPermission() { + delegate.checkAbortPermission(); + } + + public boolean hasAbortPermission() { + return delegate.hasAbortPermission(); + } + + public String getDisplayName() { + return delegate.getDisplayName(); + } + + public ResourceList getResourceList() { + return delegate.getResourceList(); + } + + // Queue.Task hashCode and equals need to be implemented to provide value equality semantics + public abstract int hashCode(); + + public abstract boolean equals(Object obj); +} diff --git a/core/src/main/java/hudson/util/VariableResolver.java b/core/src/main/java/hudson/util/VariableResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..5b01eaa652840c764f3e7cabc27dcce8dd28b197 --- /dev/null +++ b/core/src/main/java/hudson/util/VariableResolver.java @@ -0,0 +1,78 @@ +package hudson.util; + +import java.util.Map; +import java.util.Collection; + +/** + * Resolves variables to its value, while encapsulating + * how that resolution happens. + * + * @author Kohsuke Kawaguchi + */ +public interface VariableResolver { + /** + * Receives a variable name and obtains the value associated with the name. + * + *

+ * This can be implemented simply on top of a {@link Map} (see {@link ByMap}), or + * this can be used like an expression evaluator. + * + * @param name + * Name of the variable to be resolved. + * Never null, never empty. The name shouldn't include the syntactic + * marker of an expression. IOW, it should be "foo" but not "${foo}". + * A part of the goal of this design is to abstract away the expression + * marker syntax. + * @return + * Object referenced by the name. + * Null if not found. + */ + V resolve(String name); + + /** + * Empty resolver that always returns null. + */ + public static final VariableResolver NONE = new VariableResolver() { + public Object resolve(String name) { + return null; + } + }; + + /** + * {@link VariableResolver} backed by a {@link Map}. + */ + public static final class ByMap implements VariableResolver { + private final Map data; + + public ByMap(Map data) { + this.data = data; + } + + public V resolve(String name) { + return data.get(name); + } + } + + /** + * Union of multiple {@link VariableResolver}. + */ + public static final class Union implements VariableResolver { + private final VariableResolver[] resolvers; + + public Union(VariableResolver... resolvers) { + this.resolvers = resolvers; + } + + public Union(Collection> resolvers) { + this.resolvers = resolvers.toArray(new VariableResolver[resolvers.size()]); + } + + public V resolve(String name) { + for (VariableResolver r : resolvers) { + V v = r.resolve(name); + if(v!=null) return v; + } + return null; + } + } +} diff --git a/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly b/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly index 3797dcee96fa4e43e6bcbfe6efb7d05912bfa028..6c96005e43022365b6ab5b4e7e4ea45241be5d75 100644 --- a/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly +++ b/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly @@ -1,7 +1,7 @@ - + @@ -16,7 +16,8 @@ - + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/security/AuthorizationMatrixProperty/table.css b/core/src/main/resources/hudson/security/AuthorizationMatrixProperty/table.css new file mode 100644 index 0000000000000000000000000000000000000000..4c9c0edd25112d090e93001bbaf21a88f25c09a4 --- /dev/null +++ b/core/src/main/resources/hudson/security/AuthorizationMatrixProperty/table.css @@ -0,0 +1,33 @@ +#global-matrix-authorization-strategy-table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #D3D7CF; +} + +#global-matrix-authorization-strategy-table TH { + padding: 0.2em; +} + +#global-matrix-authorization-strategy-table TD.blank { + vertical-align: middle; + padding: 0.2em; +} + +#global-matrix-authorization-strategy-table .caption-row TH { + font-weight: bold; +} + +#global-matrix-authorization-strategy-table TD { + border: 1px solid #D3D7CF; +} + +#global-matrix-authorization-strategy-table TD.left-most { + text-align: left; + border-left: none; +} + +#global-matrix-authorization-strategy-table TD.stop { + border-top: 1px solid white; + border-right: 1px solid white; + border-bottom: 1px solid white; +} diff --git a/core/src/main/resources/hudson/security/ProjectMatrixAuthorizationStrategy/config.jelly b/core/src/main/resources/hudson/security/ProjectMatrixAuthorizationStrategy/config.jelly new file mode 100644 index 0000000000000000000000000000000000000000..0170d708d8fdbb2ff505eb999d1ed35ba4e54714 --- /dev/null +++ b/core/src/main/resources/hudson/security/ProjectMatrixAuthorizationStrategy/config.jelly @@ -0,0 +1,115 @@ + + + + + + + ${title} + + + + + + + + + + + remove + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ${%User/group} + + ${g.title} + +
+ ${p.name} +
+

+ ${%User/group to add}: + + +
+ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/security/ProjectMatrixAuthorizationStrategy/table.css b/core/src/main/resources/hudson/security/ProjectMatrixAuthorizationStrategy/table.css new file mode 100644 index 0000000000000000000000000000000000000000..4c9c0edd25112d090e93001bbaf21a88f25c09a4 --- /dev/null +++ b/core/src/main/resources/hudson/security/ProjectMatrixAuthorizationStrategy/table.css @@ -0,0 +1,33 @@ +#global-matrix-authorization-strategy-table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #D3D7CF; +} + +#global-matrix-authorization-strategy-table TH { + padding: 0.2em; +} + +#global-matrix-authorization-strategy-table TD.blank { + vertical-align: middle; + padding: 0.2em; +} + +#global-matrix-authorization-strategy-table .caption-row TH { + font-weight: bold; +} + +#global-matrix-authorization-strategy-table TD { + border: 1px solid #D3D7CF; +} + +#global-matrix-authorization-strategy-table TD.left-most { + text-align: left; + border-left: none; +} + +#global-matrix-authorization-strategy-table TD.stop { + border-top: 1px solid white; + border-right: 1px solid white; + border-bottom: 1px solid white; +} diff --git a/war/resources/help/project-config/parameterized-build.html b/war/resources/help/project-config/parameterized-build.html new file mode 100644 index 0000000000000000000000000000000000000000..1db569992558933221e695def679c3c1203eb731 --- /dev/null +++ b/war/resources/help/project-config/parameterized-build.html @@ -0,0 +1,14 @@ +
+ When you are using Hudson for various automations, it's sometimes convenient + to be able to "parameterize" a build, by requiring a set of user inputs to + be made available to the build process. For example, you might be setting up + an on-demand test job, where the user can submit a zip file of the binaries to be tested. + +

+ This section configures what parameters your build takes. Parameters are distinguished + by their names, and so you can have multiple parameters provided that they have different names. + +

+ See the Wiki topic + for more discussions about this feature. +

\ No newline at end of file diff --git a/war/resources/images/24x24/document-properties.gif b/war/resources/images/24x24/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..3ea89af61d8d0177eced906ccfd467d2b5f4e96b Binary files /dev/null and b/war/resources/images/24x24/document-properties.gif differ