package hudson.plugins.promoted_builds;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Cause.UserCause;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.Result;
import hudson.plugins.promoted_builds.conditions.ManualCondition;
import hudson.util.Iterators;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Promotion status of a build wrt a specific {@link PromotionProcess}.
*
* @author Kohsuke Kawaguchi
* @see PromotedBuildAction#statuses
*/
@ExportedBean
public final class Status {
/**
* Matches with {@link PromotionProcess#name}.
*/
public final String name;
private final PromotionBadge[] badges;
/**
* When did the build qualify for a promotion?
*/
public final Calendar timestamp = new GregorianCalendar();
/**
* If the build is successfully promoted, the build number of {@link Promotion}
* that represents that record.
*
* -1 to indicate that the promotion was not successful yet.
*/
private int promotion = -1;
/**
* Bulid numbers of {@link Promotion}s that are attempted.
* If {@link Promotion} fails, this field can have multiple values.
* Sorted in the ascending order.
*/
private List<Integer> promotionAttempts = new ArrayList<Integer>();
/*package*/ transient PromotedBuildAction parent;
public Status(PromotionProcess process, Collection<? extends PromotionBadge> badges) {
this.name = process.getName();
this.badges = badges.toArray(new PromotionBadge[badges.size()]);
}
@Exported
public String getName() {
return name;
}
/**
* Gets the parent {@link Status} that owns this object.
* @return Promoted build action if it exists in {@link #getTarget()} result.
*/
@CheckForNull
public PromotedBuildAction getParent() {
if (parent==null){
final AbstractBuild<?, ?> target = getTarget();
if (target != null) {
parent = target.getAction(PromotedBuildAction.class);
}
}
return parent;
}
/**
* Gets the {@link PromotionProcess} that this object deals with.
* @return Gets the promotion process for the status.
*/
@Exported
@CheckForNull
public PromotionProcess getProcess() {
assert parent != null : name;
AbstractProject<?,?> project = parent.getProject();
assert project != null : parent;
JobPropertyImpl jp = project.getProperty(JobPropertyImpl.class);
if(jp==null) return null;
return jp.getItem(name);
}
/**
* Gets the icon that should represent this promotion (that is potentially attempted but failed.)
* @param size size of the icon, will be used in the icon path
* @return Path to the icon in resources
*/
@Nonnull
public String getIcon(String size) {
String baseName;
PromotionProcess p = getProcess();
if (p == null) {
// promotion process undefined (perhaps deleted?). fallback to the default icon
baseName = "star-gold";
} else {
Promotion l = getLast();
if (l!=null && l.getResult()!= Result.SUCCESS) {
return Jenkins.RESOURCE_PATH+"/images/"+size+"/error.png";
}
baseName = p.getIcon();
}
return Jenkins.RESOURCE_PATH+"/plugin/promoted-builds/icons/"+size+"/"+ baseName +".png";
}
//TODO: what is the Null status?
/**
* Gets the build that was qualified for a promotion.
* @return Build reference
*/
@CheckForNull
public AbstractBuild<?,?> getTarget() {
final PromotedBuildAction _parent = getParent();
return _parent != null ? _parent.owner : null;
}
/**
* Called by {@link Promotion} to allow status to contribute environment variables.
*
* @param build
* The calling build. Never null.
* @param env
* Environment variables should be added to this map.
*/
public void buildEnvVars(AbstractBuild<?,?> build, EnvVars env) {
for (PromotionBadge badge : badges) {
badge.buildEnvVars(build, env);
}
}
/**
* Gets the string that says how long since this promotion had happened.
*
* @return
* string like "3 minutes" "1 day" etc.
*/
public String getTimestampString() {
long duration = new GregorianCalendar().getTimeInMillis()-timestamp.getTimeInMillis();
return Util.getTimeSpanString(duration);
}
/**
* Gets the string that says how long did it took for this build to be promoted.
* @param owner Build
* @return Time span string formatted by {@link Util#getTimeSpanString(long)}
*/
public String getDelayString(AbstractBuild<?,?> owner) {
long duration = timestamp.getTimeInMillis() - owner.getTimestamp().getTimeInMillis() - owner.getDuration();
return Util.getTimeSpanString(duration);
}
public boolean isFor(PromotionProcess process) {
return process.getName().equals(this.name);
}
/**
* Returns the {@link Promotion} object that represents the successful promotion.
*
* @param jp Job property
* @return
* {@code null} if the promotion has never been successful, or if it was but
* the record is already lost.
*/
@CheckForNull
public Promotion getSuccessfulPromotion(JobPropertyImpl jp) {
if(promotion>=0) {
PromotionProcess p = jp.getItem(name);
if(p!=null)
return p.getBuildByNumber(promotion);
}
return null;
}
/**
* Returns true if the promotion was successfully completed.
* @return {@code true} if the there were successful promotions.
*/
public boolean isPromotionSuccessful() {
return promotion>=0;
}
/**
* Checks promotion attempts.
* @return
* {@code true} if at least one {@link Promotion} activity is attempted.
* {@code false} if none is executed yet (this includes the case where it's in the queue.
*/
public boolean isPromotionAttempted() {
return !promotionAttempts.isEmpty();
}
/**
* Check if the build is in queue.
* @return {@code true} if the promotion for this is pending in the queue,
* waiting to be executed.
*/
public boolean isInQueue() {
PromotionProcess p = getProcess();
AbstractBuild<?, ?> target = getTarget();
return p != null && target != null && p.isInQueue(target);
}
/**
* Gets the badges indicating how did a build qualify for a promotion.
* @return List of promotion badges
*/
@Exported
public List<PromotionBadge> getBadges() {
return Arrays.asList(badges);
}
/**
* Called when a new promotion attempts for this build starts.
* @param p Promotion
*/
/*package*/ void addPromotionAttempt(Promotion p) {
promotionAttempts.add(p.getNumber());
}
/**
* Called when a promotion succeeds.
* @param p Promotion
*/
/*package*/ void onSuccessfulPromotion(Promotion p) {
promotion = p.getNumber();
}
//
// web bound methods
//
/**
* Gets the last successful {@link Promotion}.
* @return Last successful promotion or {@code null} if there is no successful ones.
*/
@CheckForNull
public Promotion getLastSuccessful() {
PromotionProcess p = getProcess();
if (p == null) {
return null;
}
for( Integer n : Iterators.reverse(promotionAttempts) ) {
Promotion b = p.getBuildByNumber(n);
if(b!=null && b.getResult()== Result.SUCCESS)
return b;
}
return null;
}
/**
* Gets the last failed {@link Promotion}.
* @return Last failed promotion or {@code null} if there is no failed ones.
*/
@CheckForNull
public Promotion getLastFailed() {
PromotionProcess p = getProcess();
if (p == null) {
return null;
}
for( Integer n : Iterators.reverse(promotionAttempts) ) {
Promotion b = p.getBuildByNumber(n);
if(b!=null && b.getResult()!=Result.SUCCESS)
return b;
}
return null;
}
/**
* Gets the last {@link Promotion}.
* @return Last promotion or {@code null} if there is no promotions.
*/
@CheckForNull
public Promotion getLast() {
PromotionProcess p = getProcess();
if (p == null) {
return null;
}
for( Integer n : Iterators.reverse(promotionAttempts) ) {
Promotion b = p.getBuildByNumber(n);
if(b!=null)
return b;
}
return null;
}
@Restricted(NoExternalUse.class)
public Boolean isLastAnError() {
Promotion l = getLast();
return (l != null && l.getResult() != Result.SUCCESS);
}
/**
* Gets all the promotion builds.
* @return List of promotions
*/
@Exported
public List<Promotion> getPromotionBuilds() {
List<Promotion> builds = new ArrayList<Promotion>();
PromotionProcess p = getProcess();
if (p!=null) {
for( Integer n : Iterators.reverse(promotionAttempts) ) {
Promotion b = p.getBuildByNumber(n);
if (b != null) {
builds.add(b);
}
}
}
return builds;
}
/**
* Gets the promotion build by build number.
*
* @param number build number
* @return promotion build
*/
@CheckForNull
public Promotion getPromotionBuild(int number) {
PromotionProcess p = getProcess();
return p != null ? p.getBuildByNumber(number) : null;
}
public boolean isManuallyApproved(){
final PromotionProcess process = getProcess();
if (process == null) {
return false; // Should not be processed
}
ManualCondition manualCondition=(ManualCondition) process.getPromotionCondition(ManualCondition.class.getName());
return manualCondition != null;
}
/**
* Schedules a new build.
* @param req Request
* @param rsp Response
* @throws IOException Functional error
* @throws ServletException Request handling error
*/
public void doBuild(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
final PromotionProcess process = getProcess();
if (process == null) {
throw new AbortException("Cannot retrieve the promotion process");
}
AbstractBuild<?, ?> target = getTarget();
if (target ==null) {
throw new AbortException("Cannot get the target build to be promoted");
}
ManualCondition manualCondition = (ManualCondition) process.getPromotionCondition(ManualCondition.class.getName());
if(!target.hasPermission(Promotion.PROMOTE)) {
if (manualCondition == null || (!manualCondition.getUsersAsSet().isEmpty() && !manualCondition.isInUsersList()
&& !manualCondition.isInGroupList()))
return;
}
JSONObject formData = req.getSubmittedForm();
List<ParameterValue> paramValues=null;
if (formData!=null){
paramValues = new ArrayList<ParameterValue>();
if (manualCondition!=null){
List<ParameterDefinition> parameterDefinitions=manualCondition.getParameterDefinitions();
if (parameterDefinitions != null && !parameterDefinitions.isEmpty()) {
JSONArray a = JSONArray.fromObject(formData.get("parameter"));
for (Object o : a) {
JSONObject jo = (JSONObject) o;
String name = jo.getString("name");
ParameterDefinition d = manualCondition.getParameterDefinition(name);
if (d==null)
throw new IllegalArgumentException("No such parameter definition: " + name);
paramValues.add(d.createValue(req, jo));
}
}
}
}
if (paramValues==null){
paramValues = new ArrayList<ParameterValue>();
}
Future<Promotion> f = process.scheduleBuild2(target, new UserCause(), paramValues);
if (f==null)
LOGGER.warning("Failing to schedule the promotion of "+target);
// TODO: we need better visual feed back so that the user knows that the build happened.
rsp.forwardToPreviousPage(req);
}
private static final Logger LOGGER = Logger.getLogger(Status.class.getName());
}