hudson.model.Fingerprint.java Source code

Java tutorial

Introduction

Here is the source code for hudson.model.Fingerprint.java

Source

/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc.
 * 
 * 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.google.common.collect.ImmutableList;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.basic.DateConverter;
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.Extension;
import hudson.model.listeners.ItemListener;
import hudson.model.listeners.SaveableListener;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.util.AtomicFileWriter;
import hudson.util.HexBinaryConverter;
import hudson.util.Iterators;
import hudson.util.PersistedList;
import hudson.util.RunList;
import hudson.util.XStream2;
import java.io.EOFException;
import jenkins.model.FingerprintFacet;
import jenkins.model.Jenkins;
import jenkins.model.TransientFingerprintFacetFactory;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.Authentication;
import org.xmlpull.v1.XmlPullParserException;

/**
 * A file being tracked by Jenkins.
 *
 * <p>
 * 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 {
        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}.
         * 
         * @return A name of the job
         */
        @Exported
        @Nonnull
        public String getName() {
            return name;
        }

        /**
         * Checks if the current user has permission to see this pointer.
         * @return {@code true} if the job exists and user has {@link Item#READ} permissions
         *      or if the current user has {@link Jenkins#ADMINISTER} permissions. 
         *      If the job exists, but the current user has no permission to discover it, 
         *      {@code false}  will be returned.
         *      If the job has been deleted and the user has no {@link Jenkins#ADMINISTER} permissions,
         *      it also returns {@code false}   in order to avoid the job existence fact exposure.
         */
        private boolean hasPermissionToDiscoverBuild() {
            // We expose the data to Jenkins administrators in order to
            // let them manage the data for deleted jobs (also works for SYSTEM)
            final Jenkins instance = Jenkins.getInstance();
            if (instance.hasPermission(Jenkins.ADMINISTER)) {
                return true;
            }

            return canDiscoverItem(name);
        }

        void setName(String newName) {
            name = newName;
        }

        /**
         * Gets the {@link Job} that this pointer points to,
         * or null if such a job no longer exists.
         */
        @WithBridgeMethods(value = AbstractProject.class, castRequired = true)
        public Job<?, ?> getJob() {
            return Jenkins.getInstance().getItemByFullName(name, Job.class);
        }

        /**
         * Gets the project build number.
         * <p>
         * Such {@link Run} could be since then discarded.
         * @return A build number
         */
        @Exported
        @Nonnull
        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.
         *
         * <p>
         * This is useful to check if an artifact in MavenModule
         * belongs to MavenModuleSet. 
         */
        public boolean belongsTo(Job job) {
            Item p = Jenkins.getInstance().getItemByFullName(name);
            while (p != null) {
                if (p == job)
                    return true;

                // go up the chain while we
                ItemGroup<? extends Item> parent = p.getParent();
                if (!(parent instanceof Item)) {
                    return false;
                }

                p = (Item) parent;
            }

            return false;
        }

        @Override
        public String toString() {
            return name + " #" + number;
        }
    }

    /**
     * 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 < end;
            this.start = start;
            this.end = end;
        }

        @Exported
        public int getStart() {
            return start;
        }

        @Exported
        public int getEnd() {
            return end;
        }

        public boolean isSmallerThan(int i) {
            return end <= i;
        }

        public boolean isBiggerThan(int i) {
            return i < start;
        }

        public boolean includes(int i) {
            return start <= i && i < end;
        }

        public Range expandRight() {
            return new Range(start, end + 1);
        }

        public Range expandLeft() {
            return new Range(start - 1, end);
        }

        public boolean isAdjacentTo(Range that) {
            return this.end == that.start;
        }

        @Override
        public String toString() {
            return "[" + start + "," + end + ")";
        }

        /**
         * Returns true if two {@link Range}s can't be combined into a single range.
         */
        public boolean isIndependent(Range that) {
            return this.end < that.start || that.end < this.start;
        }

        /**
         * Returns true if two {@link Range}s do not share any common integer.
         */
        public boolean isDisjoint(Range that) {
            return this.end <= that.start || that.end <= this.start;
        }

        /**
         * Returns true if this range only represents a single number.
         */
        public boolean isSingle() {
            return end - 1 == start;
        }

        /**
         * If this range contains every int that's in the other range, return true
         */
        public boolean contains(Range that) {
            return this.start <= that.start && that.end <= this.end;
        }

        /**
         * Returns the {@link Range} that combines two ranges.
         */
        public Range combine(Range that) {
            assert !isIndependent(that);
            return new Range(Math.min(this.start, that.start), Math.max(this.end, that.end));
        }

        /**
         * Returns the {@link Range} that represents the intersection of the two.
         */
        public Range intersect(Range that) {
            assert !isDisjoint(that);
            return new Range(Math.max(this.start, that.start), Math.min(this.end, that.end));
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            Range that = (Range) o;
            return start == that.start && end == that.end;

        }

        @Override
        public int hashCode() {
            return 31 * start + end;
        }
    }

    /**
     * Set of {@link Range}s. Mutable.
     */
    @ExportedBean(defaultVisibility = 3)
    public static final class RangeSet {
        // sorted
        private final List<Range> ranges;

        public RangeSet() {
            this(new ArrayList<Range>());
        }

        private RangeSet(List<Range> data) {
            this.ranges = data;
        }

        private RangeSet(Range initial) {
            this();
            ranges.add(initial);
        }

        /**
         * List all numbers in this range set, in the ascending order.
         */
        public Iterable<Integer> listNumbers() {
            final List<Range> ranges = getRanges();
            return new Iterable<Integer>() {
                public Iterator<Integer> iterator() {
                    return new Iterators.FlattenIterator<Integer, Range>(ranges) {
                        protected Iterator<Integer> expand(Range range) {
                            return Iterators.sequence(range.start, range.end).iterator();
                        }
                    };
                }
            };
        }

        //        /**
        //         * List up builds.
        //         */
        //        public <J extends Job<J,R>,R extends Run<J,R>>  Iterable<R> listBuilds(final J job) {
        //            return new Iterable<R>() {
        //                public Iterator<R> iterator() {
        //                    return new Iterators.FilterIterator<R>(new AdaptedIterator<Integer,R>(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<Integer> listNumbersReverse() {
            final List<Range> ranges = getRanges();
            return new Iterable<Integer>() {
                public Iterator<Integer> iterator() {
                    return new Iterators.FlattenIterator<Integer, Range>(Iterators.reverse(ranges)) {
                        protected Iterator<Integer> expand(Range range) {
                            return Iterators.reverseSequence(range.start, range.end).iterator();
                        }
                    };
                }
            };
        }

        /**
         * Gets all the ranges.
         */
        @Exported
        public synchronized List<Range> getRanges() {
            return new ArrayList<Range>(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; i < ranges.size(); i++) {
                Range r = ranges.get(i);
                if (r.includes(n))
                    return; // already included
                if (r.end == n) {
                    ranges.set(i, r.expandRight());
                    checkCollapse(i);
                    return;
                }
                if (r.start == n + 1) {
                    ranges.set(i, r.expandLeft());
                    checkCollapse(i - 1);
                    return;
                }
                if (r.isBiggerThan(n)) {
                    // needs to insert a single-value Range
                    ranges.add(i, new Range(n, n + 1));
                    return;
                }
            }

            ranges.add(new Range(n, n + 1));
        }

        public synchronized void addAll(int... n) {
            for (int i : n)
                add(i);
        }

        private void checkCollapse(int i) {
            if (i < 0 || i == ranges.size() - 1)
                return;
            Range lhs = ranges.get(i);
            Range rhs = ranges.get(i + 1);
            if (lhs.isAdjacentTo(rhs)) {
                // collapsed
                Range r = new Range(lhs.start, rhs.end);
                ranges.set(i, r);
                ranges.remove(i + 1);
            }
        }

        public synchronized boolean includes(int i) {
            for (Range r : ranges) {
                if (r.includes(i))
                    return true;
            }
            return false;
        }

        public synchronized void add(RangeSet that) {
            int lhs = 0, rhs = 0;
            while (lhs < this.ranges.size() && rhs < that.ranges.size()) {
                Range lr = this.ranges.get(lhs);
                Range rr = that.ranges.get(rhs);

                // no overlap
                if (lr.end < rr.start) {
                    lhs++;
                    continue;
                }
                if (rr.end < lr.start) {
                    ranges.add(lhs, rr);
                    lhs++;
                    rhs++;
                    continue;
                }

                // overlap. merge two
                Range m = lr.combine(rr);
                rhs++;

                // since ranges[lhs] is expanded, it might overlap with others in this.ranges
                while (lhs + 1 < this.ranges.size() && !m.isIndependent(this.ranges.get(lhs + 1))) {
                    m = m.combine(this.ranges.get(lhs + 1));
                    this.ranges.remove(lhs + 1);
                }

                this.ranges.set(lhs, m);
            }

            // if anything is left in that.ranges, add them all
            this.ranges.addAll(that.ranges.subList(rhs, that.ranges.size()));
        }

        /**
         * Updates this range set by the intersection of this range and the given range.
         *
         * @return true if this range set was modified as a result.
         */
        public synchronized boolean retainAll(RangeSet that) {
            List<Range> intersection = new ArrayList<Range>();

            int lhs = 0, rhs = 0;
            while (lhs < this.ranges.size() && rhs < that.ranges.size()) {
                Range lr = this.ranges.get(lhs);
                Range rr = that.ranges.get(rhs);

                if (lr.end <= rr.start) {// lr has no overlap with that.ranges
                    lhs++;
                    continue;
                }
                if (rr.end <= lr.start) {// rr has no overlap with this.ranges
                    rhs++;
                    continue;
                }

                // overlap. figure out the intersection
                Range v = lr.intersect(rr);
                intersection.add(v);

                // move on to the next pair
                if (lr.end < rr.end) {
                    lhs++;
                } else {
                    rhs++;
                }
            }

            boolean same = this.ranges.equals(intersection);

            if (!same) {
                this.ranges.clear();
                this.ranges.addAll(intersection);
                return true;
            } else {
                return false;
            }
        }

        /**
         * Updates this range set by removing all the values in the given range set.
         *
         * @return true if this range set was modified as a result.
         */
        public synchronized boolean removeAll(RangeSet that) {
            boolean modified = false;
            List<Range> sub = new ArrayList<Range>();

            int lhs = 0, rhs = 0;
            while (lhs < this.ranges.size() && rhs < that.ranges.size()) {
                Range lr = this.ranges.get(lhs);
                Range rr = that.ranges.get(rhs);

                if (lr.end <= rr.start) {// lr has no overlap with that.ranges. lr stays
                    sub.add(lr);
                    lhs++;
                    continue;
                }
                if (rr.end <= lr.start) {// rr has no overlap with this.ranges
                    rhs++;
                    continue;
                }

                // some overlap between lr and rr
                assert !lr.isDisjoint(rr);
                modified = true;

                if (rr.contains(lr)) {
                    // lr completely removed by rr
                    lhs++;
                    continue;
                }

                // we want to look at A and B below, if they are non-null.
                // |------------| lr
                //     |-----|    rr
                //   A         B
                //
                // note that lr and rr could be something like or the other way around
                // |------------| lr
                //         |------------| rr
                //     A             (no B)

                if (lr.start < rr.start) {// if A is non-empty, that will stay
                    Range a = new Range(lr.start, rr.start);
                    sub.add(a);
                }

                if (rr.end < lr.end) {// if B is non-empty
                    // we still need to check that with that.ranges, so keep it in the place of lr.
                    // how much of them will eventually stay is up to the remainder of that.ranges
                    this.ranges.set(lhs, new Range(rr.end, lr.end));
                    rhs++;
                } else {
                    // if B is empty, we are done considering lr
                    lhs++;
                }
            }

            if (!modified)
                return false; // no changes

            // whatever that remains in lhs will survive
            sub.addAll(this.ranges.subList(lhs, this.ranges.size()));

            this.ranges.clear();
            this.ranges.addAll(sub);
            return true;
        }

        @Override
        public synchronized String toString() {
            StringBuilder buf = new StringBuilder();
            for (Range r : ranges) {
                if (buf.length() > 0)
                    buf.append(',');
                buf.append(r);
            }
            return buf.toString();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            return ranges.equals(((RangeSet) o).ranges);

        }

        @Override
        public int hashCode() {
            return ranges.hashCode();
        }

        public synchronized boolean isEmpty() {
            return ranges.isEmpty();
        }

        /**
         * Returns the smallest value in this range.
         * <p>
         * 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.
         * <p>
         * 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();

            // Reject malformed ranges like "1---10", "1,,,,3" etc.
            if (list.contains("--") || list.contains(",,")) {
                if (!skipError) {
                    throw new IllegalArgumentException(
                            String.format("Unable to parse '%s', expected correct notation M,N or M-N", list));
                }
                // ignore malformed notation
                return rs;
            }

            String[] items = Util.tokenize(list, ",");
            if (items.length > 1 && items.length <= StringUtils.countMatches(list, ",")) {
                if (!skipError) {
                    throw new IllegalArgumentException(
                            String.format("Unable to parse '%s', expected correct notation M,N or M-N", list));
                }
                // ignore malformed notation like ",1,2" or "1,2,"
                return rs;
            }

            for (String s : items) {
                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.isEmpty()) {
                        if (!skipError) {
                            throw new IllegalArgumentException(
                                    String.format("Unable to parse '%s', expected number", list));
                        }
                        // ignore "" element
                        continue;
                    }

                    if (s.contains("-")) {
                        if (StringUtils.countMatches(s, "-") > 1) {
                            if (!skipError) {
                                throw new IllegalArgumentException(String.format(
                                        "Unable to parse '%s', expected correct notation M,N or M-N", list));
                            }
                            // ignore malformed ranges like "-5-2" or "2-5-"
                            continue;
                        }
                        String[] tokens = Util.tokenize(s, "-");
                        if (tokens.length == 2) {
                            int left = Integer.parseInt(tokens[0]);
                            int right = Integer.parseInt(tokens[1]);
                            if (left < 0 || right < 0) {
                                if (!skipError) {
                                    throw new IllegalArgumentException(String
                                            .format("Unable to parse '%s', expected number above zero", list));
                                }
                                // ignore a range which starts or ends under zero like "-5-3"
                                continue;
                            }
                            if (left > right) {
                                if (!skipError) {
                                    throw new IllegalArgumentException(String.format(
                                            "Unable to parse '%s', expected string with a range M-N where M<N",
                                            list));
                                }
                                // ignore inverse range like "10-5"
                                continue;
                            }
                            rs.ranges.add(new Range(left, right + 1));
                        } else {
                            if (!skipError) {
                                throw new IllegalArgumentException(String
                                        .format("Unable to parse '%s', expected string with a range M-N", list));
                            }
                            // ignore malformed text like "1-10-50"
                            continue;
                        }
                    } else {
                        int n = Integer.parseInt(s);
                        rs.ranges.add(new Range(n, n + 1));
                    }
                } catch (NumberFormatException e) {
                    if (!skipError)
                        throw new IllegalArgumentException(
                                String.format("Unable to parse '%s', expected number", 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;
                writer.setValue(serialize(src));
            }

            static String serialize(RangeSet src) {
                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);
                }
                return buf.toString();
            }

            public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
                if (reader.hasMoreChildren()) {
                    /* old format where <range> elements are nested like
                        <range>
                          <start>1337</start>
                          <end>1479</end>
                        </range>
                     */
                    return new RangeSet((List<Range>) (collectionConv.unmarshal(reader, context)));
                } else {
                    return RangeSet.fromString(reader.getValue(), true);
                }
            }
        }
    }

    @Extension
    public static final class ProjectRenameListener extends ItemListener {
        @Override
        public void onLocationChanged(final Item item, final String oldName, final String newName) {
            try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
                locationChanged(item, oldName, newName);
            }
        }

        private void locationChanged(Item item, String oldName, String newName) {
            if (item instanceof Job) {
                Job p = Jenkins.getInstance().getItemByFullName(newName, Job.class);
                if (p != null) {
                    RunList<? extends Run> builds = p.getBuilds();
                    for (Run build : builds) {
                        Collection<Fingerprint> fingerprints = build.getBuildFingerprints();
                        for (Fingerprint f : fingerprints) {
                            try {
                                f.rename(oldName, newName);
                            } catch (IOException e) {
                                logger.log(Level.WARNING, "Failed to update fingerprint record " + f.getFileName()
                                        + " when " + oldName + " was renamed to " + newName, e);
                            }
                        }
                    }
                }
            }
        }
    }

    private static final DateConverter DATE_CONVERTER = new DateConverter();

    /**
     * Time when the fingerprint has been captured.
     */
    private final @Nonnull Date timestamp;

    /**
     * Null if this fingerprint is for a file that's
     * apparently produced outside.
     */
    private final @CheckForNull 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<String, RangeSet> usages = new Hashtable<String, RangeSet>();

    PersistedList<FingerprintFacet> facets = new PersistedList<FingerprintFacet>(this);

    /**
     * Lazily computed immutable {@link FingerprintFacet}s created from {@link TransientFingerprintFacetFactory}.
     */
    private transient volatile List<FingerprintFacet> transientFacets = null;

    public Fingerprint(@CheckForNull Run build, @Nonnull String fileName, @Nonnull byte[] md5sum)
            throws IOException {
        this(build == null ? null : new BuildPtr(build), fileName, md5sum);
        save();
    }

    Fingerprint(@CheckForNull BuildPtr original, @Nonnull String fileName, @Nonnull byte[] md5sum) {
        this.original = original;
        this.md5sum = md5sum;
        this.fileName = fileName;
        this.timestamp = new Date();
    }

    /**
     * The first build in which this file showed up,
     * if the file looked like it's created there.
     * <p>
     * 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 or if the current
     *      user has no permission to discover the job.
     */
    @Exported
    public @CheckForNull BuildPtr getOriginal() {
        if (original != null && original.hasPermissionToDiscoverBuild()) {
            return original;
        }
        return null;
    }

    public @Nonnull String getDisplayName() {
        return fileName;
    }

    /**
     * The file name (like "foo.jar" without path).
     */
    @Exported
    public @Nonnull String getFileName() {
        return fileName;
    }

    /**
     * Gets the MD5 hash string.
     */
    @Exported(name = "hash")
    public @Nonnull String getHashString() {
        return Util.toHexString(md5sum);
    }

    /**
     * Gets the timestamp when this record is created.
     */
    @Exported
    public @Nonnull 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 @Nonnull String getTimestampString() {
        long duration = System.currentTimeMillis() - timestamp.getTime();
        return Util.getPastTimeString(duration);
    }

    /**
     * Gets the build range set for the given job name.
     *
     * <p>
     * These builds of this job has used this file.
     * @return may be empty but not null.
     */
    public @Nonnull 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 @Nonnull List<String> getJobs() {
        List<String> r = new ArrayList<String>();
        r.addAll(usages.keySet());
        Collections.sort(r);
        return r;
    }

    public @Nonnull Hashtable<String, RangeSet> getUsages() {
        return usages;
    }

    @ExportedBean(defaultVisibility = 2)
    public static final class RangeItem {
        @Exported
        public final String name;
        @Exported
        public final RangeSet ranges;

        public RangeItem(String name, RangeSet ranges) {
            this.name = name;
            this.ranges = ranges;
        }
    }

    // this is for remote API
    @Exported(name = "usage")
    public @Nonnull List<RangeItem> _getUsages() {
        List<RangeItem> r = new ArrayList<RangeItem>();
        final Jenkins instance = Jenkins.getInstance();
        for (Entry<String, RangeSet> e : usages.entrySet()) {
            final String itemName = e.getKey();
            if (instance.hasPermission(Jenkins.ADMINISTER) || canDiscoverItem(itemName)) {
                r.add(new RangeItem(itemName, e.getValue()));
            }
        }
        return r;
    }

    /**
     * @deprecated Use {@link #addFor(hudson.model.Run)}
     */
    @Deprecated
    public synchronized void add(@Nonnull AbstractBuild b) throws IOException {
        addFor((Run) b);
    }

    /**
     * Adds a usage reference to the build.
     * @param b {@link Run} to be referenced in {@link #usages}
     * @since 1.577
     */
    public synchronized void addFor(@Nonnull Run b) throws IOException {
        add(b.getParent().getFullName(), b.getNumber());
    }

    /**
     * Records that a build of a job has used this file.
     */
    public synchronized void add(@Nonnull String jobFullName, int n) throws IOException {
        addWithoutSaving(jobFullName, n);
        save();
    }

    void addWithoutSaving(@Nonnull String jobFullName, int n) {
        synchronized (usages) { // TODO why not synchronized (this) like some, though not all, other accesses?
            RangeSet r = usages.get(jobFullName);
            if (r == null) {
                r = new RangeSet();
                usages.put(jobFullName, r);
            }
            r.add(n);
        }
    }

    /**
     * Returns true if any of the builds recorded in this fingerprint
     * is still retained.
     *
     * <p>
     * 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<String, RangeSet> e : usages.entrySet()) {
            Job j = Jenkins.getInstance().getItemByFullName(e.getKey(), Job.class);
            if (j == null)
                continue;

            Run firstBuild = j.getFirstBuild();
            if (firstBuild == null)
                continue;

            int oldest = firstBuild.getNumber();
            if (!e.getValue().isSmallerThan(oldest))
                return true;
        }
        return false;
    }

    /**
     * Trim off references to non-existent builds and jobs, thereby making the fingerprint smaller.
     *
     * @return true
     *      if this record was modified.
     * 
     * @throws IOException Save failure
     */
    public synchronized boolean trim() throws IOException {
        boolean modified = false;

        for (Entry<String, RangeSet> e : new Hashtable<String, RangeSet>(usages).entrySet()) {// copy because we mutate
            Job j = Jenkins.getInstance().getItemByFullName(e.getKey(), Job.class);
            if (j == null) {// no such job any more. recycle the record
                modified = true;
                usages.remove(e.getKey());
                continue;
            }

            Run firstBuild = j.getFirstBuild();
            if (firstBuild == null) {// no builds. recycle the whole record
                modified = true;
                usages.remove(e.getKey());
                continue;
            }

            RangeSet cur = e.getValue();

            // builds that are around without the keepLog flag on are normally clustered together (in terms of build #)
            // so our basic strategy is to discard everything up to the first ephemeral build, except those builds
            // that are marked as kept
            RangeSet kept = new RangeSet();
            Run r = firstBuild;
            while (r != null && r.isKeepLog()) {
                kept.add(r.getNumber());
                r = r.getNextBuild();
            }

            if (r == null) {
                // all the build records are permanently kept ones, so we'll just have to keep 'kept' out of whatever currently in 'cur'
                modified |= cur.retainAll(kept);
            } else {
                // otherwise we are ready to discard [0,r.number) except those marked as 'kept'
                RangeSet discarding = new RangeSet(new Range(-1, r.getNumber()));
                discarding.removeAll(kept);
                modified |= cur.removeAll(discarding);
            }

            if (cur.isEmpty()) {
                usages.remove(e.getKey());
                modified = true;
            }
        }

        if (modified) {
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "Saving trimmed {0}", getFingerprintFile(md5sum));
            }
            save();
        }

        return modified;
    }

    /**
     * Gets the associated {@link FingerprintFacet}s.
     *
     * <p>
     * This method always return a non-empty collection, which is a synthetic collection.
     * It contains persisted {@link FingerprintFacet}s (those that are added explicitly, like
     * {@code fingerprint.getFacets().add(x)}), as well those {@linkplain TransientFingerprintFacetFactory that are transient}.
     *
     * <p>
     * Mutation to this collection will manipulate persisted set of {@link FingerprintFacet}s, and therefore regardless
     * of what you do, this collection will always contain a set of {@link FingerprintFacet}s that are added
     * by {@link TransientFingerprintFacetFactory}s.
     *
     * @since 1.421
     */
    public @Nonnull Collection<FingerprintFacet> getFacets() {
        if (transientFacets == null) {
            List<FingerprintFacet> transientFacets = new ArrayList<FingerprintFacet>();
            for (TransientFingerprintFacetFactory fff : TransientFingerprintFacetFactory.all()) {
                fff.createFor(this, transientFacets);
            }
            this.transientFacets = ImmutableList.copyOf(transientFacets);
        }

        return new AbstractCollection<FingerprintFacet>() {
            @Override
            public Iterator<FingerprintFacet> iterator() {
                return Iterators.sequence(facets.iterator(), transientFacets.iterator());
            }

            @Override
            public boolean add(FingerprintFacet e) {
                facets.add(e);
                return true;
            }

            @Override
            public boolean remove(Object o) {
                return facets.remove(o);
            }

            @Override
            public boolean contains(Object o) {
                return facets.contains(o) || transientFacets.contains(o);
            }

            @Override
            public int size() {
                return facets.size() + transientFacets.size();
            }
        };
    }

    /**
     * Sorts {@link FingerprintFacet}s by their timestamps.
     * @return Sorted list of {@link FingerprintFacet}s 
     */
    public @Nonnull Collection<FingerprintFacet> getSortedFacets() {
        List<FingerprintFacet> r = new ArrayList<FingerprintFacet>(getFacets());
        Collections.sort(r, new Comparator<FingerprintFacet>() {
            public int compare(FingerprintFacet o1, FingerprintFacet o2) {
                long a = o1.getTimestamp();
                long b = o2.getTimestamp();
                if (a < b)
                    return -1;
                if (a == b)
                    return 0;
                return 1;
            }
        });
        return r;
    }

    /**
     * Finds a facet of the specific type (including subtypes.)
     * @param <T> Class of the {@link FingerprintFacet}
     * @return First matching facet of the specified class
     * @since 1.556
     */
    public @CheckForNull <T extends FingerprintFacet> T getFacet(Class<T> type) {
        for (FingerprintFacet f : getFacets()) {
            if (type.isInstance(f))
                return type.cast(f);
        }
        return null;
    }

    /**
     * Returns the actions contributed from {@link #getFacets()}
     */
    public @Nonnull List<Action> getActions() {
        List<Action> r = new ArrayList<Action>();
        for (FingerprintFacet ff : getFacets())
            ff.createActions(r);
        return Collections.unmodifiableList(r);
    }

    /**
     * Save the settings to a file.
     * @throws IOException Save error
     */
    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);
        save(file);
        SaveableListener.fireOnChange(this, getConfigFile(file));

        if (logger.isLoggable(Level.FINE))
            logger.fine("Saving fingerprint " + file + " took " + (System.currentTimeMillis() - start) + "ms");
    }

    void save(File file) throws IOException {
        if (facets.isEmpty()) {
            file.getParentFile().mkdirs();
            // JENKINS-16301: fast path for the common case.
            AtomicFileWriter afw = new AtomicFileWriter(file);
            try {
                PrintWriter w = new PrintWriter(afw);
                w.println("<?xml version='1.1' encoding='UTF-8'?>");
                w.println("<fingerprint>");
                w.print("  <timestamp>");
                w.print(DATE_CONVERTER.toString(timestamp));
                w.println("</timestamp>");
                if (original != null) {
                    w.println("  <original>");
                    w.print("    <name>");
                    w.print(Util.xmlEscape(original.name));
                    w.println("</name>");
                    w.print("    <number>");
                    w.print(original.number);
                    w.println("</number>");
                    w.println("  </original>");
                }
                w.print("  <md5sum>");
                w.print(Util.toHexString(md5sum));
                w.println("</md5sum>");
                w.print("  <fileName>");
                w.print(Util.xmlEscape(fileName));
                w.println("</fileName>");
                w.println("  <usages>");
                for (Map.Entry<String, RangeSet> e : usages.entrySet()) {
                    w.println("    <entry>");
                    w.print("      <string>");
                    w.print(Util.xmlEscape(e.getKey()));
                    w.println("</string>");
                    w.print("      <ranges>");
                    w.print(RangeSet.ConverterImpl.serialize(e.getValue()));
                    w.println("</ranges>");
                    w.println("    </entry>");
                }
                w.println("  </usages>");
                w.println("  <facets/>");
                w.print("</fingerprint>");
                w.flush();
                afw.commit();
            } finally {
                afw.abort();
            }
        } else {
            // Slower fallback that can persist facets.
            getConfigFile(file).write(this);
        }
    }

    /**
     * Update references to a renamed job in the fingerprint
     */
    public synchronized void rename(String oldName, String newName) throws IOException {
        boolean touched = false;
        if (original != null) {
            if (original.getName().equals(oldName)) {
                original.setName(newName);
                touched = true;
            }
        }

        if (usages != null) {
            RangeSet r = usages.get(oldName);
            if (r != null) {
                usages.put(newName, r);
                usages.remove(oldName);
                touched = true;
            }
        }

        if (touched) {
            save();
        }
    }

    public Api getApi() {
        return new Api(this);
    }

    /**
     * The file we save our configuration.
     */
    private static @Nonnull XmlFile getConfigFile(@Nonnull File file) {
        return new XmlFile(XSTREAM, file);
    }

    /**
     * Determines the file name from md5sum.
     */
    private static @Nonnull File getFingerprintFile(@Nonnull byte[] md5sum) {
        assert md5sum.length == 16;
        return new File(Jenkins.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.
     * @return Loaded {@link Fingerprint}. Null if the config file does not exist or
     * malformed.
     */
    /*package*/ static @CheckForNull Fingerprint load(@Nonnull byte[] md5sum) throws IOException {
        return load(getFingerprintFile(md5sum));
    }

    /*package*/ static @CheckForNull Fingerprint load(@Nonnull 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 {
            Object loaded = configFile.read();
            if (!(loaded instanceof Fingerprint)) {
                throw new IOException("Unexpected Fingerprint type. Expected " + Fingerprint.class
                        + " or subclass but got " + (loaded != null ? loaded.getClass() : "null"));
            }
            Fingerprint f = (Fingerprint) loaded;
            if (logger.isLoggable(Level.FINE))
                logger.fine("Loading fingerprint " + file + " took " + (System.currentTimeMillis() - start) + "ms");
            if (f.facets == null)
                f.facets = new PersistedList<FingerprintFacet>(f);
            for (FingerprintFacet facet : f.facets)
                facet._setOwner(f);
            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? {0}", configFile);
                file.delete();
                return null;
            }
            String parseError = messageOfParseException(e);
            if (parseError != null) {
                logger.log(Level.WARNING, "Malformed XML in {0}: {1}", new Object[] { configFile, parseError });
                file.delete();
                return null;
            }
            logger.log(Level.WARNING, "Failed to load " + configFile, e);
            throw e;
        }
    }

    private static String messageOfParseException(Throwable t) {
        if (t instanceof XmlPullParserException || t instanceof EOFException) {
            return t.getMessage();
        }
        Throwable t2 = t.getCause();
        if (t2 != null) {
            return messageOfParseException(t2);
        } else {
            return null;
        }
    }

    @Override
    public String toString() {
        return "Fingerprint[original=" + original + ",hash=" + getHashString() + ",fileName=" + fileName
                + ",timestamp=" + DATE_CONVERTER.toString(timestamp) + ",usages="
                + new TreeMap<String, RangeSet>(usages) + ",facets=" + facets + "]";
    }

    /**
     * Checks if the current user can Discover the item.
     * If yes, it may be displayed as a text in Fingerprint UIs.
     * @param fullName Full name of the job
     * @return {@code true} if the user can discover the item
     */
    private static boolean canDiscoverItem(@Nonnull final String fullName) {
        final Jenkins jenkins = Jenkins.getInstance();

        // Fast check to avoid security context switches
        Item item = null;
        try {
            item = jenkins.getItemByFullName(fullName);
        } catch (AccessDeniedException ex) {
            // ignore, we will fall-back later
        }
        if (item != null) {
            return true;
        }

        // Probably it failed due to the missing Item.DISCOVER
        // We try to retrieve the job using SYSTEM user and to check permissions manually.
        final Authentication userAuth = Jenkins.getAuthentication();
        try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
            final Item itemBySystemUser = jenkins.getItemByFullName(fullName);
            if (itemBySystemUser == null) {
                return false;
            }

            // To get the item existence fact, a user needs Item.DISCOVER for the item
            // and Item.READ for all container folders.
            boolean canDiscoverTheItem = itemBySystemUser.hasPermission(userAuth, Item.DISCOVER);
            if (canDiscoverTheItem) {
                ItemGroup<?> current = itemBySystemUser.getParent();
                do {
                    if (current instanceof Item) {
                        final Item i = (Item) current;
                        current = i.getParent();
                        if (!i.hasPermission(userAuth, Item.READ)) {
                            canDiscoverTheItem = false;
                        }
                    } else {
                        current = null;
                    }
                } while (canDiscoverTheItem && current != null);
            }
            return canDiscoverTheItem;
        }
    }

    private static final XStream2 XSTREAM = new XStream2();

    /**
     * Provides the XStream instance this class is using for serialization.
     *
     * @return the XStream instance
     * @since 1.655
     */
    @Nonnull
    public static XStream2 getXStream() {
        return XSTREAM;
    }

    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());
}