/*
* The MIT License
*
* Copyright (c) 2011-2013, CloudBees, Inc., Stephen Connolly.
*
* 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.cloudbees.hudson.plugins.folder.computed;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.Job;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.tasks.LogRotator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* The default {@link OrphanedItemStrategy}.
* Trims dead items by the # of days or the # of builds much like {@link LogRotator}.
*
* @author Stephen Connolly
*/
public class DefaultOrphanedItemStrategy extends OrphanedItemStrategy {
/**
* Should old branches be removed at all
*/
private final boolean pruneDeadBranches;
/**
* If not -1, dead branches are only kept up to this days.
*/
private final int daysToKeep;
/**
* If not -1, only this number of dead branches are kept.
*/
private final int numToKeep;
/**
* Stapler's constructor.
*
* @param pruneDeadBranches remove dead branches.
* @param daysToKeepStr how old a branch must be to remove.
* @param numToKeepStr how many branches to keep.
*/
@DataBoundConstructor
public DefaultOrphanedItemStrategy(boolean pruneDeadBranches, @CheckForNull String daysToKeepStr,
@CheckForNull String numToKeepStr) {
this.pruneDeadBranches = pruneDeadBranches;
// TODO in lieu of DeadBranchCleanupThread, introduce a form warning if daysToKeep < PeriodicFolderTrigger.interval
this.daysToKeep = pruneDeadBranches ? fromString(daysToKeepStr) : -1;
this.numToKeep = pruneDeadBranches ? fromString(numToKeepStr) : -1;
}
/**
* Gets the number of days to keep dead branches.
*
* @return the number of days to keep dead branches.
*/
@SuppressWarnings("unused") // used by Jelly EL
public int getDaysToKeep() {
return daysToKeep;
}
/**
* Gets the number of dead branches to keep.
*
* @return the number of dead branches to keep.
*/
@SuppressWarnings("unused") // used by Jelly EL
public int getNumToKeep() {
return numToKeep;
}
/**
* Returns {@code true} if dead branches should be removed.
*
* @return {@code true} if dead branches should be removed.
*/
@SuppressWarnings("unused") // used by Jelly EL
public boolean isPruneDeadBranches() {
return pruneDeadBranches;
}
/**
* Returns the number of days to keep dead branches.
*
* @return the number of days to keep dead branches.
*/
@SuppressWarnings("unused") // used by Jelly EL
@NonNull
public String getDaysToKeepStr() {
return toString(daysToKeep);
}
/**
* Gets the number of dead branches to keep.
*
* @return the number of dead branches to keep.
*/
@SuppressWarnings("unused") // used by Jelly EL
@NonNull
public String getNumToKeepStr() {
return toString(numToKeep);
}
/**
* Helper to turn a int into a string where {@code -1} correspond to the empty string.
*
* @param i the possibly {@code null} {@link Integer}
* @return the {@link String} representation of the int.
*/
@NonNull
private static String toString(int i) {
if (i == -1) {
return "";
}
return String.valueOf(i);
}
/**
* Inverse of {@link #toString(int)}.
*
* @param s the string.
* @return the int.
*/
private static int fromString(@CheckForNull String s) {
if (StringUtils.isBlank(s)) {
return -1;
}
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return -1;
}
}
private static long lastBuildTime(TopLevelItem item) {
long t = 0;
for (Job<?,?> j : item.getAllJobs()) {
Run<?,?> b = j.getLastBuild();
if (b != null) {
t = Math.max(t, b.getTimeInMillis());
}
}
return t;
}
@Override
public <I extends TopLevelItem> Collection<I> orphanedItems(ComputedFolder<I> owner, Collection<I> orphaned, TaskListener listener) throws IOException, InterruptedException {
List<I> toRemove = new ArrayList<I>();
if (pruneDeadBranches && (numToKeep != -1 || daysToKeep != -1)) {
listener.getLogger().printf("Evaluating orphaned items in %s%n", owner.getFullDisplayName());
List<I> candidates = new ArrayList<I>(orphaned);
Collections.sort(candidates, new Comparator<I>() {
@Override
public int compare(I i1, I i2) {
// most recent build first
long ms1 = lastBuildTime(i1);
long ms2 = lastBuildTime(i2);
return (ms2 < ms1) ? -1 : ((ms2 == ms1) ? 0 : 1); // TODO Java 7+: Long.compare(ms2, ms1);
}
});
CANDIDATES: for (Iterator<I> iterator = candidates.iterator(); iterator.hasNext();) {
I item = iterator.next();
for (Job<?,?> job : item.getAllJobs()) {
// Enumerating all builds is inefficient. But we will most likely delete this job anyway,
// which will have a cost proportional to the number of builds just to delete those files.
for (Run<?,?> build : job.getBuilds()) {
if (build.isBuilding()) {
listener.getLogger().printf("Will not remove %s as %s is still in progress%n", item.getDisplayName(), build.getFullDisplayName());
iterator.remove();
continue CANDIDATES;
}
String whyKeepLog = build.getWhyKeepLog();
if (whyKeepLog != null) {
listener.getLogger().printf("Will not remove %s as %s is marked to not be removed: %s%n", item.getDisplayName(), build.getFullDisplayName(), whyKeepLog);
iterator.remove();
continue CANDIDATES;
}
}
}
}
int count = 0;
if (numToKeep != -1) {
for (Iterator<I> iterator = candidates.iterator(); iterator.hasNext(); ) {
I item = iterator.next();
count++;
if (count <= numToKeep) {
listener.getLogger().printf("Will not remove %s as it is only #%d in the list%n", item.getDisplayName(), count);
continue;
}
listener.getLogger().printf("Will remove %s as it is #%d in the list%n", item.getDisplayName(), count);
toRemove.add(item);
iterator.remove();
}
}
if (daysToKeep != -1) {
Calendar cal = new GregorianCalendar();
cal.add(Calendar.DAY_OF_YEAR, -daysToKeep);
for (I item : candidates) {
if (lastBuildTime(item) > cal.getTimeInMillis()) {
listener.getLogger().printf("Will not remove %s because it is new%n", item.getDisplayName());
continue;
}
listener.getLogger().printf("Will remove %s as it is too old%n", item.getDisplayName());
toRemove.add(item);
}
}
}
return toRemove;
}
/**
* Our {@link hudson.model.Descriptor}
*/
@Extension
@SuppressWarnings("unused") // instantiated by Jenkins
public static class DescriptorImpl extends OrphanedItemStrategyDescriptor {
/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return "Default";
}
}
}