/* * The MIT License * * Copyright (c) 2009, Sun Microsystems, Inc., 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 com.robestone.hudson.compactcolumns; import hudson.Messages; import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import hudson.views.ListViewColumn; import hudson.views.ListViewColumnDescriptor; import java.math.BigDecimal; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; public abstract class AbstractCompactColumn extends ListViewColumn { // copied from hudson.Util because they were private private static final long ONE_SECOND_MS = 1000; private static final long ONE_MINUTE_MS = 60 * ONE_SECOND_MS; private static final long ONE_HOUR_MS = 60 * ONE_MINUTE_MS; static final long ONE_DAY_MS = 24 * ONE_HOUR_MS; private static final long ONE_MONTH_MS = 30 * ONE_DAY_MS; private static final long ONE_YEAR_MS = 365 * ONE_DAY_MS; public boolean isBuildsEmpty(Job<?, ?> job) { // TODO -- make much more efficient return getBuilds(job).isEmpty(); } public List<BuildInfo> getBuilds(Job<?, ?> job) { List<BuildInfo> builds = new ArrayList<BuildInfo>(); addNonNull(builds, getLastFailedBuild(job)); addNonNull(builds, getLastUnstableBuild(job)); addNonNull(builds, getLastStableBuild(job)); if (builds.isEmpty()) { BuildInfo aborted = createBuildInfo(getLastAbortedBuild(job), "gray", "Aborted", null, job); addNonNull(builds, aborted); } Collections.sort(builds); for (int i = 0; i < builds.size(); i++) { BuildInfo info = builds.get(i); info.setFirst(i == 0); info.setMultipleBuilds(builds.size() > 1); } return builds; } /** * @param onlyIfLastCompleted When the statuses aren't sorted, we only show the last failed * when it is also the latest completed build. */ public BuildInfo getLastFailedBuild(Job<?, ?> job) { boolean onlyIfLastCompleted = isFailedShownOnlyIfLast(); Run<?, ?> lastFailedBuild = job.getLastFailedBuild(); Run<?, ?> lastCompletedBuild = job.getLastCompletedBuild(); if (lastFailedBuild == null) { return null; } else if (!onlyIfLastCompleted || (lastCompletedBuild.number == lastFailedBuild.number)) { return createBuildInfo(job.getLastFailedBuild(), "red", "Failed", "lastFailedBuild", job); } else { return null; } } abstract protected boolean isFailedShownOnlyIfLast(); abstract protected boolean isUnstableShownOnlyIfLast(); public BuildInfo getLastStableBuild(Job<?, ?> job) { return createBuildInfo(job.getLastStableBuild(), "blue", "Stable", "lastStableBuild", job); } public BuildInfo getLastUnstableBuild(Job<?, ?> job) { Run<?, ?> lastUnstable = null; Run<?, ?> latest = job.getLastBuild(); while (latest != null) { if (latest.getResult() == Result.UNSTABLE) { lastUnstable = latest; break; } latest = latest.getPreviousBuild(); } if (lastUnstable == null) { return null; } Run<?, ?> lastCompleted = job.getLastCompletedBuild(); boolean isLastCompleted = (lastCompleted != null && lastCompleted.number == lastUnstable.number); if (isUnstableShownOnlyIfLast() && !isLastCompleted) { return null; } String unstableColor = "orange"; // best color that is "yellow" but visible too return createBuildInfo(lastUnstable, unstableColor, "Unstable", String.valueOf(lastUnstable.number), job); } protected void addNonNull(List<BuildInfo> builds, BuildInfo info) { if (info != null) { builds.add(info); } } private Run<?, ?> getLastAbortedBuild(Job<?, ?> job) { Run<?, ?> latest = job.getLastBuild(); while (latest != null) { if (latest.getResult() == Result.ABORTED) { return latest; } latest = latest.getPreviousBuild(); } return null; } private BuildInfo createBuildInfo(Run<?, ?> run, String color, String status, String urlPart, Job<?, ?> job) { if (run != null) { String timeAgoString = getTimeAgoString(run.getTimeInMillis()); long buildTime = run.getTime().getTime(); if (urlPart == null) { urlPart = String.valueOf(run.number); } Run<?, ?> latest = job.getLastBuild(); BuildInfo build = new BuildInfo( run, color, timeAgoString, buildTime, status, urlPart, run.number == latest.number); return build; } return null; } protected String getTimeAgoString(long timestamp) { long now = System.currentTimeMillis(); float diff = now - timestamp; String stime = getShortTimestamp(diff); return stime; } protected static String getBuildTimeString(long timeMs, Locale locale) { Date time = new Date(timeMs); DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale); String datePattern = getDatePattern(locale); DateFormat dateFormat = new SimpleDateFormat(datePattern, locale); String timeString = timeFormat.format(time); String dateString = dateFormat.format(time); String dateTimeString = timeString + ", " + dateString; return dateTimeString; } /** * I want to use 4-digit years (for clarity), and that doesn't work out of the box... */ protected static String getDatePattern(Locale locale) { DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, locale); if (format instanceof SimpleDateFormat) { String s = ((SimpleDateFormat) format).toPattern(); if (!s.contains("yyyy")) { s = s.replace("yy", "yyyy"); } return s; } else { // shown by unit test to not be a problem... throw new IllegalArgumentException("Can't handle locale: " + locale); } } /** * Avoids having "2 days 3 hours" and instead does "2.1 days". * * Additional strategy details: * < 1 sec = 0 sec * < 10 of anything = x.y of that (scale 1) * >= 10 of anything = x (scale 0) */ protected String getShortTimestamp(float time) { String ts; float number; if (time >= ONE_YEAR_MS) { number = getRoundedNumber(time / ONE_YEAR_MS); ts = Messages.Util_year(number); } else if (time >= ONE_MONTH_MS) { number = getRoundedNumber(time / ONE_MONTH_MS); ts = Messages.Util_month(number); } else if (time >= ONE_DAY_MS) { number = getRoundedNumber(time / ONE_DAY_MS); ts = Messages.Util_day(number); } else if (time >= ONE_HOUR_MS) { number = getRoundedNumber(time / ONE_HOUR_MS); ts = Messages.Util_hour(number); } else if (time >= ONE_MINUTE_MS) { number = getRoundedNumber(time / ONE_MINUTE_MS); ts = Messages.Util_minute(number); } else if (time >= ONE_SECOND_MS) { number = getRoundedNumber(time / ONE_SECOND_MS); ts = Messages.Util_second(number); } else { ts = Messages.Util_second(0); } return ts; } protected float getRoundedNumber(float number) { int scale; if (number >= 10) { scale = 0; } else { scale = 1; } return new BigDecimal(number).setScale(scale, BigDecimal.ROUND_HALF_DOWN).floatValue(); } public abstract static class AbstractCompactColumnDescriptor extends ListViewColumnDescriptor { @Override public boolean shownByDefault() { return false; } } }