/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* 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 jenkins.model;
import hudson.Util;
import hudson.model.Action;
import hudson.model.BuildableItem;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.ParametersAction;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.model.queue.QueueTaskFuture;
import hudson.search.SearchIndexBuilder;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.AlternativeUiTextProvider;
import hudson.views.BuildButtonColumn;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.servlet.ServletException;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import jenkins.util.TimeDuration;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Allows a {@link Job} to make use of {@link ParametersDefinitionProperty} and be scheduled in various ways.
* Stateless so there is no need to keep an instance of it in a field.
* @since 1.556
*/
@SuppressWarnings("unchecked") // AbstractItem.getParent does not correctly override; scheduleBuild2 inherently untypable
public abstract class ParameterizedJobMixIn<JobT extends Job<JobT, RunT> & ParameterizedJobMixIn.ParameterizedJob & Queue.Task, RunT extends Run<JobT, RunT> & Queue.Executable> {
protected abstract JobT asJob();
/** @see BuildableItem#scheduleBuild() */
@SuppressWarnings("deprecation")
public final boolean scheduleBuild() {
return scheduleBuild(asJob().getQuietPeriod(), new Cause.LegacyCodeCause());
}
/** @see BuildableItem#scheduleBuild(Cause) */
public final boolean scheduleBuild(Cause c) {
return scheduleBuild(asJob().getQuietPeriod(), c);
}
/** @see BuildableItem#scheduleBuild(int) */
@SuppressWarnings("deprecation")
public final boolean scheduleBuild(int quietPeriod) {
return scheduleBuild(quietPeriod, new Cause.LegacyCodeCause());
}
/** @see BuildableItem#scheduleBuild(int, Cause) */
public final boolean scheduleBuild(int quietPeriod, Cause c) {
return scheduleBuild2(quietPeriod, c != null ? Collections.<Action>singletonList(new CauseAction(c)) : Collections.<Action>emptyList()) != null;
}
/**
* Provides a standard implementation of an optional method of the same name in a {@link Job} type to schedule a build with the ability to wait for its result.
* That job method is often used during functional tests ({@code JenkinsRule.assertBuildStatusSuccess}).
* @param quietPeriod seconds to wait before starting (normally 0)
* @param actions various actions to associate with the scheduling, such as {@link ParametersAction} or {@link CauseAction}
* @return a handle by which you may wait for the build to complete (or just start); or null if the build was not actually scheduled for some reason
*/
public final @CheckForNull QueueTaskFuture<RunT> scheduleBuild2(int quietPeriod, Action... actions) {
Queue.Item i = scheduleBuild2(quietPeriod, Arrays.asList(actions));
return i != null ? (QueueTaskFuture) i.getFuture() : null;
}
/**
* Convenience method to schedule a build.
* Useful for {@link Trigger} implementations, for example.
* If you need to wait for the build to start (or finish), use {@link Queue.Item#getFuture}.
* @param job a job which might be schedulable
* @param quietPeriod seconds to wait before starting; use {@code -1} to use the job’s default settings
* @param actions various actions to associate with the scheduling, such as {@link ParametersAction} or {@link CauseAction}
* @return a newly created, or reused, queue item if the job could be scheduled; null if it was refused for some reason (e.g., some {@link Queue.QueueDecisionHandler} rejected it), or if {@code job} is not a {@link ParameterizedJob} or it is not {@link Job#isBuildable})
* @since 1.621
*/
public static @CheckForNull Queue.Item scheduleBuild2(final Job<?,?> job, int quietPeriod, Action... actions) {
if (!(job instanceof ParameterizedJob)) {
return null;
}
return new ParameterizedJobMixIn() {
@Override protected Job asJob() {
return job;
}
}.scheduleBuild2(quietPeriod == -1 ? ((ParameterizedJob) job).getQuietPeriod() : quietPeriod, Arrays.asList(actions));
}
@CheckForNull Queue.Item scheduleBuild2(int quietPeriod, List<Action> actions) {
if (!asJob().isBuildable())
return null;
List<Action> queueActions = new ArrayList<Action>(actions);
if (isParameterized() && Util.filter(queueActions, ParametersAction.class).isEmpty()) {
queueActions.add(new ParametersAction(getDefaultParametersValues()));
}
return Jenkins.getInstance().getQueue().schedule2(asJob(), quietPeriod, queueActions).getItem();
}
private List<ParameterValue> getDefaultParametersValues() {
ParametersDefinitionProperty paramDefProp = asJob().getProperty(ParametersDefinitionProperty.class);
ArrayList<ParameterValue> defValues = new ArrayList<ParameterValue>();
/*
* This check is made ONLY if someone will call this method even if isParametrized() is false.
*/
if(paramDefProp == null)
return defValues;
/* Scan for all parameter with an associated default values */
for(ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions())
{
ParameterValue defaultValue = paramDefinition.getDefaultParameterValue();
if(defaultValue != null)
defValues.add(defaultValue);
}
return defValues;
}
/**
* A job should define a method of the same signature for use from {@link BuildButtonColumn}.
*/
public final boolean isParameterized() {
return asJob().getProperty(ParametersDefinitionProperty.class) != null;
}
/**
* Schedules a new build command.
* Create a method on your job with the same signature and delegate to this.
*/
@SuppressWarnings("deprecation")
public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
if (delay == null) {
delay = new TimeDuration(asJob().getQuietPeriod());
}
if (!asJob().isBuildable()) {
throw HttpResponses.error(SC_CONFLICT, new IOException(asJob().getFullName() + " is not buildable"));
}
// if a build is parameterized, let that take over
ParametersDefinitionProperty pp = asJob().getProperty(ParametersDefinitionProperty.class);
if (pp != null && !req.getMethod().equals("POST")) {
// show the parameter entry form.
req.getView(pp, "index.jelly").forward(req, rsp);
return;
}
hudson.model.BuildAuthorizationToken.checkPermission(asJob(), asJob().getAuthToken(), req, rsp);
if (pp != null) {
pp._doBuild(req, rsp, delay);
return;
}
Queue.Item item = Jenkins.getInstance().getQueue().schedule2(asJob(), delay.getTime(), getBuildCause(asJob(), req)).getItem();
if (item != null) {
rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl());
} else {
rsp.sendRedirect(".");
}
}
/**
* Supports build trigger with parameters via an HTTP GET or POST.
* Currently only String parameters are supported.
* Create a method on your job with the same signature and delegate to this.
*/
@SuppressWarnings("deprecation")
public final void doBuildWithParameters(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
hudson.model.BuildAuthorizationToken.checkPermission(asJob(), asJob().getAuthToken(), req, rsp);
ParametersDefinitionProperty pp = asJob().getProperty(ParametersDefinitionProperty.class);
if (!asJob().isBuildable()) {
throw HttpResponses.error(SC_CONFLICT, new IOException(asJob().getFullName() + " is not buildable!"));
}
if (pp != null) {
pp.buildWithParameters(req, rsp, delay);
} else {
throw new IllegalStateException("This build is not parameterized!");
}
}
/**
* Cancels a scheduled build.
* Create a method on your job marked {@link RequirePOST} but with the same signature and delegate to this.
*/
@RequirePOST
public final void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
asJob().checkPermission(Item.CANCEL);
Jenkins.getInstance().getQueue().cancel(asJob());
rsp.forwardToPreviousPage(req);
}
/**
* Use from a {@link Job#makeSearchIndex} override.
* @param sib the super value
* @return the value to return
*/
public final SearchIndexBuilder extendSearchIndex(SearchIndexBuilder sib) {
if (asJob().isBuildable() && asJob().hasPermission(Item.BUILD)) {
sib.add("build", "build");
}
return sib;
}
/**
* Computes the build cause, using RemoteCause or UserCause as appropriate.
*/
@Restricted(NoExternalUse.class)
public static final CauseAction getBuildCause(ParameterizedJob job, StaplerRequest req) {
Cause cause;
@SuppressWarnings("deprecation")
hudson.model.BuildAuthorizationToken authToken = job.getAuthToken();
if (authToken != null && authToken.getToken() != null && req.getParameter("token") != null) {
// Optional additional cause text when starting via token
String causeText = req.getParameter("cause");
cause = new Cause.RemoteCause(req.getRemoteAddr(), causeText);
} else {
cause = new Cause.UserIdCause();
}
return new CauseAction(cause);
}
/**
* Allows customization of the human-readable display name to be rendered in the <i>Build Now</i> link.
* @see #getBuildNowText
* @since TODO
*/
public static final AlternativeUiTextProvider.Message<ParameterizedJob> BUILD_NOW_TEXT = new AlternativeUiTextProvider.Message<ParameterizedJob>();
/**
* Suggested implementation of {@link ParameterizedJob#getBuildNowText}.
* Uses {@link #BUILD_NOW_TEXT}.
*/
public final String getBuildNowText() {
return isParameterized() ? Messages.ParameterizedJobMixIn_build_with_parameters() : AlternativeUiTextProvider.get(BUILD_NOW_TEXT, asJob(), Messages.ParameterizedJobMixIn_build_now());
}
/**
* Checks for the existence of a specific trigger on a job.
* @param <T> a trigger type
* @param job a job
* @param clazz the type of the trigger
* @return a configured trigger of the requested type, or null if there is none such, or {@code job} is not a {@link ParameterizedJob}
* @since 1.621
*/
public static @CheckForNull <T extends Trigger<?>> T getTrigger(Job<?,?> job, Class<T> clazz) {
if (!(job instanceof ParameterizedJob)) {
return null;
}
for (Trigger<?> t : ((ParameterizedJob) job).getTriggers().values()) {
if (clazz.isInstance(t)) {
return clazz.cast(t);
}
}
return null;
}
/**
* Marker for job using this mixin.
*/
public interface ParameterizedJob extends hudson.model.Queue.Task, hudson.model.Item {
@SuppressWarnings("deprecation")
@CheckForNull hudson.model.BuildAuthorizationToken getAuthToken();
int getQuietPeriod();
String getBuildNowText();
/**
* Gets currently configured triggers.
* You may use {@code <p:config-trigger/>} to configure them.
* @return a map from trigger kind to instance
* @see #getTrigger
*/
Map<TriggerDescriptor,Trigger<?>> getTriggers();
}
}