All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.model.Fingerprint Maven / Gradle / Ivy

The newest version!
/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 * 
 * 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 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.BulkChange;
import hudson.model.listeners.SaveableListener;
import hudson.util.HexBinaryConverter;
import hudson.util.Iterators;
import hudson.util.XStream2;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

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.Iterator;
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.
 *
 * 

* Lifecycle is managed by {@link FingerprintMap}. * * @author Kohsuke Kawaguchi * @see FingerprintMap */ @ExportedBean public class Fingerprint implements ModelObject, Saveable { /** * Pointer to a {@link Build}. */ @ExportedBean(defaultVisibility=2) 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().getFullName(), run.getNumber() ); } /** * Gets {@link Job#getFullName() the full name of the job}. *

* Such job could be since then removed, * so there might not be a corresponding * {@link Job}. */ @Exported public String getName() { return name; } /** * Gets the {@link Job} that this pointer points to, * or null if such a job no longer exists. */ public AbstractProject getJob() { return Hudson.getInstance().getItemByFullName(name,AbstractProject.class); } /** * Gets the project build number. *

* Such {@link Run} could be since then * discarded. */ @Exported 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().getFullName().equals(name); } /** * Returns true if {@link BuildPtr} points to the given job. */ public boolean is(Job job) { return job.getFullName().equals(name); } /** * Returns true if {@link BuildPtr} points to the given job * or one of its subordinates. * *

* This is useful to check if an artifact in MavenModule * belongs to MavenModuleSet. */ public boolean belongsTo(Job job) { Item p = Hudson.getInstance().getItemByFullName(name); while(p!=null) { if(p==job) return true; // go up the chain while we ItemGroup parent = p.getParent(); if (!(parent instanceof Item)) { return false; } p = (Item) parent; } return false; } } /** * Range of build numbers [start,end). Immutable. */ @ExportedBean(defaultVisibility=4) 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; } /** * List all numbers in this range set, in the ascending order. */ public Iterable listNumbers() { final List ranges = getRanges(); return new Iterable() { public Iterator iterator() { return new Iterators.FlattenIterator(ranges) { protected Iterator expand(Range range) { return Iterators.sequence(range.start,range.end).iterator(); } }; } }; } // /** // * List up builds. // */ // public ,R extends Run> Iterable listBuilds(final J job) { // return new Iterable() { // public Iterator iterator() { // return new Iterators.FilterIterator(new AdaptedIterator(listNumbers().iterator()) { // protected R adapt(Integer n) { // return job.getBuildByNumber(n); // } // }) { // protected boolean filter(R r) { // return r!=null; // } // }; // } // }; // } /** * List all numbers in this range set in the descending order. */ public Iterable listNumbersReverse() { final List ranges = getRanges(); return new Iterable() { public Iterator iterator() { return new Iterators.FlattenIterator(Iterators.reverse(ranges)) { protected Iterator expand(Range range) { return Iterators.reverseSequence(range.start,range.end).iterator(); } }; } }; } /** * Gets all the ranges. */ @Exported 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 the smallest value in this range. *

* If this range is empty, this method throws an exception. */ public synchronized int min() { return ranges.get(0).start; } /** * Returns the largest value in this range. *

* If this range is empty, this method throws an exception. */ public synchronized int max() { return ranges.get(ranges.size()-1).end; } /** * 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); } /** * Parses a {@link RangeSet} from a string like "1-3,5,7-9" */ public static RangeSet fromString(String list, boolean skipError) { RangeSet rs = new RangeSet(); for (String s : Util.tokenize(list,",")) { s = s.trim(); // s is either single number or range "x-y". // note that the end range is inclusive in this notation, but not in the Range class try { if(s.contains("-")) { String[] tokens = Util.tokenize(s,"-"); rs.ranges.add(new Range(Integer.parseInt(tokens[0]),Integer.parseInt(tokens[1])+1)); } else { int n = Integer.parseInt(s); rs.ranges.add(new Range(n,n+1)); } } catch (NumberFormatException e) { if (!skipError) throw new IllegalArgumentException("Unable to parse "+list); // ignore malformed text } } return rs; } 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) { RangeSet src = (RangeSet) source; StringBuilder buf = new StringBuilder(src.ranges.size()*10); for (Range r : src.ranges) { if(buf.length()>0) buf.append(','); if(r.isSingle()) buf.append(r.start); else buf.append(r.start).append('-').append(r.end-1); } writer.setValue(buf.toString()); } public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { if(reader.hasMoreChildren()) { /* old format where elements are nested like 1337 1479 */ return new RangeSet((List)(collectionConv.unmarshal(reader,context))); } else { return RangeSet.fromString(reader.getValue(),true); } } } } 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 full 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. */ @Exported public BuildPtr getOriginal() { return original; } public String getDisplayName() { return fileName; } /** * The file name (like "foo.jar" without path). */ @Exported public String getFileName() { return fileName; } /** * Gets the MD5 hash string. */ @Exported(name="hash") public String getHashString() { return Util.toHexString(md5sum); } /** * Gets the timestamp when this record is created. */ @Exported 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.getPastTimeString(duration); } /** * Gets the build range set for the given job name. * *

* These builds of this job has used this file. */ public RangeSet getRangeSet(String jobFullName) { RangeSet r = usages.get(jobFullName); if(r==null) r = new RangeSet(); return r; } public RangeSet getRangeSet(Job job) { return getRangeSet(job.getFullName()); } /** * 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; } @ExportedBean(defaultVisibility=2) public static final class RangeItem { //TODO: review and check whether we can do it private @Exported public final String name; //TODO: review and check whether we can do it private @Exported public final RangeSet ranges; public String getName() { return name; } public RangeSet getRanges() { return ranges; } public RangeItem(String name, RangeSet ranges) { this.name = name; this.ranges = ranges; } } // this is for remote API @Exported(name="usage") public List _getUsages() { List r = new ArrayList(); for (Entry e : usages.entrySet()) r.add(new RangeItem(e.getKey(),e.getValue())); return r; } public synchronized void add(AbstractBuild b) throws IOException { add(b.getParent().getFullName(),b.getNumber()); } /** * Records that a build of a job has used this file. */ public synchronized void add(String jobFullName, int n) throws IOException { synchronized(usages) { RangeSet r = usages.get(jobFullName); if(r==null) { r = new RangeSet(); usages.put(jobFullName,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!=null && original.isAlive()) return true; for (Entry e : usages.entrySet()) { Job j = Hudson.getInstance().getItemByFullName(e.getKey(),Job.class); 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 { if(BulkChange.contains(this)) return; long start=0; if(logger.isLoggable(Level.FINE)) start = System.currentTimeMillis(); File file = getFingerprintFile(md5sum); getConfigFile(file).write(this); SaveableListener.fireOnChange(this, getConfigFile(file)); if(logger.isLoggable(Level.FINE)) logger.fine("Saving fingerprint "+file+" took "+(System.currentTimeMillis()-start)+"ms"); } public Api getApi() { return new Api(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; long start=0; if(logger.isLoggable(Level.FINE)) start = System.currentTimeMillis(); try { Fingerprint f = (Fingerprint) configFile.read(); if(logger.isLoggable(Level.FINE)) logger.fine("Loading fingerprint "+file+" took "+(System.currentTimeMillis()-start)+"ms"); return f; } catch (IOException e) { if(file.exists() && file.length()==0) { // Despite the use of AtomicFile, there are reports indicating that people often see // empty XML file, presumably either due to file system corruption (perhaps by sudden // power loss, etc.) or abnormal program termination. // generally we don't want to wipe out user data just because we can't load it, // but if the file size is 0, which is what's reported in HUDSON-2012, then it seems // like recovering it silently by deleting the file is not a bad idea. logger.log(Level.WARNING, "Size zero fingerprint. Disk corruption? "+configFile,e); file.delete(); return null; } 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.getMapper()) { @Override protected Object createCollection(Class type) { return new ArrayList(); } } ),10); } private static final Logger logger = Logger.getLogger(Fingerprint.class.getName()); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy