package com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.facade.RelativeLocation;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.pipeline.WorkflowNodeTraversal;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.Duration;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.DurationInMilliseconds;
import com.smartcodeltd.jenkinsci.plugins.buildmonitor.viewmodel.duration.HumanReadableDuration;
import hudson.model.*;
import hudson.scm.ChangeLogSet;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static com.smartcodeltd.jenkinsci.plugins.buildmonitor.functions.NullSafety.getOrElse;
public class BuildView implements BuildViewModel {
private final Run<?,?> build;
private final boolean isPipeline;
private final RelativeLocation parentJobLocation;
private final Date systemTime;
@VisibleForTesting
static BuildView of(Run<?, ?> build) {
return new BuildView(build, false, RelativeLocation.of(build.getParent()), new Date());
}
public static BuildView of(Run<?, ?> build, boolean isPipeline, RelativeLocation parentJobLocation, Date systemTime) {
return new BuildView(build, isPipeline, parentJobLocation, systemTime);
}
@Override
public String name() {
return build.getDisplayName();
}
// todo: fix the double slash added when there's no parent
@Override
public String url() {
return parentJobLocation.url() + "/" + build.getNumber() + "/";
}
@Override
public Result result() {
return build.getResult();
}
@Override
public boolean isRunning() {
return isRunning(this.build);
}
private boolean isRunning(Run<?, ?> build) {
return build.hasntStartedYet() || build.isBuilding() || build.isLogUpdated();
}
@Override
public Duration elapsedTime() {
return new HumanReadableDuration(now() - whenTheBuildStarted());
}
@Override
public Duration timeElapsedSince() {
return new DurationInMilliseconds(now() - (whenTheBuildStarted() + build.getDuration()));
}
@Override
public Duration duration() {
return new HumanReadableDuration(build.getDuration());
}
@Override
public Duration estimatedDuration() {
return new HumanReadableDuration(build.getEstimatedDuration());
}
@Override
public int progress() {
if (! isRunning()) {
return 0;
}
if (isTakingLongerThanUsual()) {
return 100;
}
long elapsedTime = now() - whenTheBuildStarted(),
estimatedDuration = build.getEstimatedDuration();
if (estimatedDuration > 0) {
return (int) ((float) elapsedTime / (float) estimatedDuration * 100);
}
return 100;
}
@Override
public String description() {
return getOrElse(build.getDescription(), "");
}
@Override
public boolean isPipeline() {
return isPipeline;
}
@Override
public List<String> pipelineStages() {
WorkflowRun currentBuild = (WorkflowRun) this.build;
FlowExecution execution = currentBuild.getExecution();
if (execution != null) {
WorkflowNodeTraversal traversal = new WorkflowNodeTraversal();
traversal.start(execution.getCurrentHeads());
return traversal.getStages();
}
return Collections.emptyList();
}
private boolean isTakingLongerThanUsual() {
return elapsedTime().greaterThan(estimatedDuration());
}
@Override
public boolean hasPreviousBuild() {
return null != build.getPreviousBuild();
}
@Override
public BuildViewModel previousBuild() {
return new BuildView(build.getPreviousBuild(), isPipeline, this.parentJobLocation, systemTime);
}
@Override
public Set<String> culprits() {
return getUsers(new Reader() {
@Override
public Iterable<String> readUsersFrom(AbstractBuild<?, ?> jenkinsBuild) {
return transform(jenkinsBuild.getCulprits(), new Function<User, String>() {
@Override
public String apply(User culprit) {
return culprit.getFullName();
}
});
}
});
}
@Override
public Set<String> committers() {
return getUsers(new Reader() {
@Override
public Iterable<String> readUsersFrom(AbstractBuild<?, ?> jenkinsBuild) {
return transform(nonNullIterable(jenkinsBuild.getChangeSet()), new Function<ChangeLogSet.Entry, String>() {
@Override
public String apply(ChangeLogSet.Entry entry) {
return entry.getAuthor().getFullName();
}
});
}
private <T> T nonNullIterable(T list) {
return (T) getOrElse(list, newArrayList());
}
});
}
@Override
public <A extends Action> Optional<A> detailsOf(Class<A> jenkinsAction) {
return Optional.fromNullable(build.getAction(jenkinsAction));
}
@Override
public <A extends Action> List<A> allDetailsOf(Class<A> jenkinsAction) {
return build.getActions(jenkinsAction);
}
@Override
public String toString() {
return name();
}
private long now() {
return systemTime.getTime();
}
private long whenTheBuildStarted() {
return build.getTimestamp().getTimeInMillis();
}
private BuildView(Run<?, ?> build, boolean isPipeline, RelativeLocation parentJobLocation, Date systemTime) {
this.build = build;
this.isPipeline = isPipeline;
this.parentJobLocation = parentJobLocation;
this.systemTime = systemTime;
}
private interface Reader {
Iterable<String> readUsersFrom(AbstractBuild<?, ?> jenkinsBuild);
}
private Set<String> getUsers(Reader reader) {
Set<String> users = new TreeSet<String>();
if (build instanceof AbstractBuild<?, ?>) {
AbstractBuild<?, ?> jenkinsBuild = (AbstractBuild<?, ?>) build;
Iterables.addAll(users, reader.readUsersFrom(jenkinsBuild));
}
return users;
}
}