hudson.model.Fingerprint Maven / Gradle / Ivy
package hudson.model;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.CollectionConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import hudson.Util;
import hudson.XmlFile;
import hudson.util.HexBinaryConverter;
import hudson.util.XStream2;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A file being tracked by Hudson.
*
* @author Kohsuke Kawaguchi
*/
public class Fingerprint implements ModelObject {
/**
* Pointer to a {@link Build}.
*/
public static class BuildPtr {
final String name;
final int number;
public BuildPtr(String name, int number) {
this.name = name;
this.number = number;
}
public BuildPtr(Run run) {
this( run.getParent().getName(), run.getNumber() );
}
/**
* Gets the name of the job.
*
* Such job could be since then removed,
* so there might not be a corresponding
* {@link Job}.
*/
public String getName() {
return name;
}
/**
* Gets the {@link Job} that this pointer points to,
* or null if such a job no longer exists.
*/
public Job getJob() {
return Hudson.getInstance().getJob(name);
}
/**
* Gets the project build number.
*
* Such {@link Run} could be since then
* discarded.
*/
public int getNumber() {
return number;
}
/**
* Gets the {@link Job} that this pointer points to,
* or null if such a job no longer exists.
*/
public Run getRun() {
Job j = getJob();
if(j==null) return null;
return j.getBuildByNumber(number);
}
private boolean isAlive() {
return getRun()!=null;
}
/**
* Returns true if {@link BuildPtr} points to the given run.
*/
public boolean is(Run r) {
return r.getNumber()==number && r.getParent().getName().equals(name);
}
/**
* Returns true if {@link BuildPtr} points to the given job.
*/
public boolean is(Job job) {
return job.getName().equals(name);
}
}
/**
* Range of build numbers [start,end). Immutable.
*/
public static final class Range {
final int start;
final int end;
public Range(int start, int end) {
assert start ranges;
public RangeSet() {
this(new ArrayList());
}
private RangeSet(List data) {
this.ranges = data;
}
/**
* Gets all the ranges.
*/
public synchronized List getRanges() {
return new ArrayList(ranges);
}
/**
* Expands the range set to include the given value.
* If the set already includes this number, this will be a no-op.
*/
public synchronized void add(int n) {
for( int i=0; i0) buf.append(',');
buf.append(r);
}
return buf.toString();
}
public synchronized boolean isEmpty() {
return ranges.isEmpty();
}
/**
* Returns true if all the integers logically in this {@link RangeSet}
* is smaller than the given integer. For example, {[1,3)} is smaller than 3,
* but {[1,3),[100,105)} is not smaller than anything less than 105.
*
* Note that {} is smaller than any n.
*/
public synchronized boolean isSmallerThan(int n) {
if(ranges.isEmpty()) return true;
return ranges.get(ranges.size() - 1).isSmallerThan(n);
}
static final class ConverterImpl implements Converter {
private final Converter collectionConv; // used to convert ArrayList in it
public ConverterImpl(Converter collectionConv) {
this.collectionConv = collectionConv;
}
public boolean canConvert(Class type) {
return type==RangeSet.class;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
collectionConv.marshal( ((RangeSet)source).getRanges(), writer, context );
}
public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
return new RangeSet((List)(collectionConv.unmarshal(reader,context)));
}
}
}
private final Date timestamp;
/**
* Null if this fingerprint is for a file that's
* apparently produced outside.
*/
private final BuildPtr original;
private final byte[] md5sum;
private final String fileName;
/**
* Range of builds that use this file keyed by a job name.
*/
private final Hashtable usages = new Hashtable();
public Fingerprint(Run build, String fileName, byte[] md5sum) throws IOException {
this.original = build==null ? null : new BuildPtr(build);
this.md5sum = md5sum;
this.fileName = fileName;
this.timestamp = new Date();
save();
}
/**
* The first build in which this file showed up,
* if the file looked like it's created there.
*
* This is considered as the "source" of this file,
* or the owner, in the sense that this project "owns"
* this file.
*
* @return null
* if the file is apparently created outside Hudson.
*/
public BuildPtr getOriginal() {
return original;
}
public String getDisplayName() {
return fileName;
}
/**
* The file name (like "foo.jar" without path).
*/
public String getFileName() {
return fileName;
}
/**
* Gets the MD5 hash string.
*/
public String getHashString() {
return Util.toHexString(md5sum);
}
/**
* Gets the timestamp when this record is created.
*/
public Date getTimestamp() {
return timestamp;
}
/**
* Gets the string that says how long since this build has scheduled.
*
* @return
* string like "3 minutes" "1 day" etc.
*/
public String getTimestampString() {
long duration = System.currentTimeMillis()-timestamp.getTime();
return Util.getTimeSpanString(duration);
}
/**
* Gets the build range set for the given job name.
*
*
* These builds of this job has used this file.
*/
public RangeSet getRangeSet(String jobName) {
RangeSet r = usages.get(jobName);
if(r==null) r = new RangeSet();
return r;
}
public RangeSet getRangeSet(Job job) {
return getRangeSet(job.getName());
}
/**
* Gets the sorted list of job names where this jar is used.
*/
public List getJobs() {
List r = new ArrayList();
r.addAll(usages.keySet());
Collections.sort(r);
return r;
}
public Hashtable getUsages() {
return usages;
}
public synchronized void add(Build b) throws IOException {
add(b.getParent().getName(),b.getNumber());
}
/**
* Records that a build of a job has used this file.
*/
public synchronized void add(String jobName, int n) throws IOException {
synchronized(usages) {
RangeSet r = usages.get(jobName);
if(r==null) {
r = new RangeSet();
usages.put(jobName,r);
}
r.add(n);
}
save();
}
/**
* Returns true if any of the builds recorded in this fingerprint
* is still retained.
*
*
* This is used to find out old fingerprint records that can be removed
* without losing too much information.
*/
public synchronized boolean isAlive() {
if(original.isAlive())
return true;
for (Entry e : usages.entrySet()) {
Job j = Hudson.getInstance().getJob(e.getKey());
if(j==null)
continue;
int oldest = j.getFirstBuild().getNumber();
if(!e.getValue().isSmallerThan(oldest))
return true;
}
return false;
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
XmlFile f = getConfigFile(getFingerprintFile(md5sum));
f.mkdirs();
f.write(this);
}
/**
* The file we save our configuration.
*/
private static XmlFile getConfigFile(File file) {
return new XmlFile(XSTREAM,file);
}
/**
* Determines the file name from md5sum.
*/
private static File getFingerprintFile(byte[] md5sum) {
assert md5sum.length==16;
return new File( Hudson.getInstance().getRootDir(),
"fingerprints/"+ Util.toHexString(md5sum,0,1)+'/'+Util.toHexString(md5sum,1,1)+'/'+Util.toHexString(md5sum,2,md5sum.length-2)+".xml");
}
/**
* Loads a {@link Fingerprint} from a file in the image.
*/
/*package*/ static Fingerprint load(byte[] md5sum) throws IOException {
return load(getFingerprintFile(md5sum));
}
/*package*/ static Fingerprint load(File file) throws IOException {
XmlFile configFile = getConfigFile(file);
if(!configFile.exists())
return null;
try {
return (Fingerprint)configFile.read();
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to load "+configFile,e);
throw e;
}
}
private static final XStream XSTREAM = new XStream2();
static {
XSTREAM.alias("fingerprint",Fingerprint.class);
XSTREAM.alias("range",Range.class);
XSTREAM.alias("ranges",RangeSet.class);
XSTREAM.registerConverter(new HexBinaryConverter(),10);
XSTREAM.registerConverter(new RangeSet.ConverterImpl(
new CollectionConverter(XSTREAM.getClassMapper()) {
protected Object createCollection(Class type) {
return new ArrayList();
}
}
),10);
}
private static final Logger logger = Logger.getLogger(Fingerprint.class.getName());
}