File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\model\Fingerprint.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc.
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 package hudson.model;
25
26 import com.google.common.collect.ImmutableList;
27 import com.google.common.collect.ImmutableListMultimap;
28 import com.thoughtworks.xstream.XStream;
29 import com.thoughtworks.xstream.converters.Converter;
30 import com.thoughtworks.xstream.converters.MarshallingContext;
31 import com.thoughtworks.xstream.converters.UnmarshallingContext;
32 import com.thoughtworks.xstream.converters.collections.CollectionConverter;
33 import com.thoughtworks.xstream.io.HierarchicalStreamReader;
34 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
35 import hudson.Util;
36 import hudson.XmlFile;
37 import hudson.BulkChange;
38 import hudson.Extension;
39 import hudson.model.listeners.ItemListener;
40 import hudson.model.listeners.SaveableListener;
41 import hudson.util.HexBinaryConverter;
42 import hudson.util.Iterators;
43 import hudson.util.PersistedList;
44 import hudson.util.RunList;
45 import hudson.util.XStream2;
46 import jenkins.model.FingerprintFacet;
47 import jenkins.model.Jenkins;
48 import jenkins.model.TransientFingerprintFacetFactory;
49 import org.kohsuke.stapler.export.Exported;
50 import org.kohsuke.stapler.export.ExportedBean;
51
52 import java.io.File;
53 import java.io.IOException;
54 import java.util.AbstractCollection;
55 import java.util.ArrayList;
56 import java.util.Collection;
57 import java.util.Collections;
58 import java.util.Comparator;
59 import java.util.Date;
60 import java.util.Hashtable;
61 import java.util.Iterator;
62 import java.util.List;
63 import java.util.Map.Entry;
64 import java.util.logging.Level;
65 import java.util.logging.Logger;
66
67 /**
68 * A file being tracked by Jenkins.
69 *
70 * <p>
71 * Lifecycle is managed by {@link FingerprintMap}.
72 *
73 * @author Kohsuke Kawaguchi
74 * @see FingerprintMap
75 */
76 @ExportedBean
77 public class Fingerprint implements ModelObject, Saveable {
78 /**
79 * Pointer to a {@link Build}.
80 */
81 @ExportedBean(defaultVisibility=2)
82 public static class BuildPtr {
83 String name;
84 final int number;
85
86 public BuildPtr(String name, int number) {
87 this.name = name;
88 this.number = number;
89 }
90
91 public BuildPtr(Run run) {
92 this( run.getParent().getFullName(), run.getNumber() );
93 }
94
95 /**
96 * Gets {@link Job#getFullName() the full name of the job}.
97 * <p>
98 * Such job could be since then removed,
99 * so there might not be a corresponding
100 * {@link Job}.
101 */
102 @Exported
103 public String getName() {
104 return name;
105 }
106
107 void setName(String newName) {
108 name = newName;
109 }
110
111 /**
112 * Gets the {@link Job} that this pointer points to,
113 * or null if such a job no longer exists.
114 */
115 public AbstractProject getJob() {
116 return Jenkins.getInstance().getItemByFullName(name,AbstractProject.class);
117 }
118
119 /**
120 * Gets the project build number.
121 * <p>
122 * Such {@link Run} could be since then
123 * discarded.
124 */
125 @Exported
126 public int getNumber() {
127 return number;
128 }
129
130 /**
131 * Gets the {@link Job} that this pointer points to,
132 * or null if such a job no longer exists.
133 */
134 public Run getRun() {
135 Job j = getJob();
136 if(j==null) return null;
137 return j.getBuildByNumber(number);
138 }
139
140 private boolean isAlive() {
141 return getRun()!=null;
142 }
143
144 /**
145 * Returns true if {@link BuildPtr} points to the given run.
146 */
147 public boolean is(Run r) {
148 return r.getNumber()==number && r.getParent().getFullName().equals(name);
149 }
150
151 /**
152 * Returns true if {@link BuildPtr} points to the given job.
153 */
154 public boolean is(Job job) {
155 return job.getFullName().equals(name);
156 }
157
158 /**
159 * Returns true if {@link BuildPtr} points to the given job
160 * or one of its subordinates.
161 *
162 * <p>
163 * This is useful to check if an artifact in MavenModule
164 * belongs to MavenModuleSet.
165 */
166 public boolean belongsTo(Job job) {
167 Item p = Jenkins.getInstance().getItemByFullName(name);
168 while(p!=null) {
169 if(p==job)
170 return true;
171
172 // go up the chain while we
173 ItemGroup<? extends Item> parent = p.getParent();
174 if (!(parent instanceof Item)) {
175 return false;
176 }
177
178 p = (Item) parent;
179 }
180
181 return false;
182 }
183 }
184
185 /**
186 * Range of build numbers [start,end). Immutable.
187 */
188 @ExportedBean(defaultVisibility=4)
189 public static final class Range {
190 final int start;
191 final int end;
192
193 public Range(int start, int end) {
194 assert start<end;
195 this.start = start;
196 this.end = end;
197 }
198
199 @Exported
200 public int getStart() {
201 return start;
202 }
203
204 @Exported
205 public int getEnd() {
206 return end;
207 }
208
209 public boolean isSmallerThan(int i) {
210 return end<=i;
211 }
212
213 public boolean isBiggerThan(int i) {
214 return i<start;
215 }
216
217 public boolean includes(int i) {
218 return start<=i && i<end;
219 }
220
221 public Range expandRight() {
222 return new Range(start,end+1);
223 }
224
225 public Range expandLeft() {
226 return new Range(start-1,end);
227 }
228
229 public boolean isAdjacentTo(Range that) {
230 return this.end==that.start;
231 }
232
233 @Override
234 public String toString() {
235 return "["+start+","+end+")";
236 }
237
238 /**
239 * Returns true if two {@link Range}s can't be combined into a single range.
240 */
241 public boolean isIndependent(Range that) {
242 return this.end<that.start ||that.end<this.start;
243 }
244
245 /**
246 * Returns true if this range only represents a single number.
247 */
248 public boolean isSingle() {
249 return end-1==start;
250 }
251
252 /**
253 * Returns the {@link Range} that combines two ranges.
254 */
255 public Range combine(Range that) {
256 assert !isIndependent(that);
257 return new Range(
258 Math.min(this.start,that.start),
259 Math.max(this.end ,that.end ));
260 }
261 }
262
263 /**
264 * Set of {@link Range}s.
265 */
266 @ExportedBean(defaultVisibility=3)
267 public static final class RangeSet {
268 // sorted
269 private final List<Range> ranges;
270
271 public RangeSet() {
272 this(new ArrayList<Range>());
273 }
274
275 private RangeSet(List<Range> data) {
276 this.ranges = data;
277 }
278
279 /**
280 * List all numbers in this range set, in the ascending order.
281 */
282 public Iterable<Integer> listNumbers() {
283 final List<Range> ranges = getRanges();
284 return new Iterable<Integer>() {
285 public Iterator<Integer> iterator() {
286 return new Iterators.FlattenIterator<Integer,Range>(ranges) {
287 protected Iterator<Integer> expand(Range range) {
288 return Iterators.sequence(range.start,range.end).iterator();
289 }
290 };
291 }
292 };
293 }
294
295 // /**
296 // * List up builds.
297 // */
298 // public <J extends Job<J,R>,R extends Run<J,R>> Iterable<R> listBuilds(final J job) {
299 // return new Iterable<R>() {
300 // public Iterator<R> iterator() {
301 // return new Iterators.FilterIterator<R>(new AdaptedIterator<Integer,R>(listNumbers().iterator()) {
302 // protected R adapt(Integer n) {
303 // return job.getBuildByNumber(n);
304 // }
305 // }) {
306 // protected boolean filter(R r) {
307 // return r!=null;
308 // }
309 // };
310 // }
311 // };
312 // }
313
314 /**
315 * List all numbers in this range set in the descending order.
316 */
317 public Iterable<Integer> listNumbersReverse() {
318 final List<Range> ranges = getRanges();
319 return new Iterable<Integer>() {
320 public Iterator<Integer> iterator() {
321 return new Iterators.FlattenIterator<Integer,Range>(Iterators.reverse(ranges)) {
322 protected Iterator<Integer> expand(Range range) {
323 return Iterators.reverseSequence(range.start,range.end).iterator();
324 }
325 };
326 }
327 };
328 }
329
330 /**
331 * Gets all the ranges.
332 */
333 @Exported
334 public synchronized List<Range> getRanges() {
335 return new ArrayList<Range>(ranges);
336 }
337
338 /**
339 * Expands the range set to include the given value.
340 * If the set already includes this number, this will be a no-op.
341 */
342 public synchronized void add(int n) {
343 for( int i=0; i<ranges.size(); i++ ) {
344 Range r = ranges.get(i);
345 if(r.includes(n)) return; // already included
346 if(r.end==n) {
347 ranges.set(i,r.expandRight());
348 checkCollapse(i);
349 return;
350 }
351 if(r.start==n+1) {
352 ranges.set(i,r.expandLeft());
353 checkCollapse(i-1);
354 return;
355 }
356 if(r.isBiggerThan(n)) {
357 // needs to insert a single-value Range
358 ranges.add(i,new Range(n,n+1));
359 return;
360 }
361 }
362
363 ranges.add(new Range(n,n+1));
364 }
365
366 private void checkCollapse(int i) {
367 if(i<0 || i==ranges.size()-1) return;
368 Range lhs = ranges.get(i);
369 Range rhs = ranges.get(i+1);
370 if(lhs.isAdjacentTo(rhs)) {
371 // collapsed
372 Range r = new Range(lhs.start,rhs.end);
373 ranges.set(i,r);
374 ranges.remove(i+1);
375 }
376 }
377
378 public synchronized boolean includes(int i) {
379 for (Range r : ranges) {
380 if(r.includes(i))
381 return true;
382 }
383 return false;
384 }
385
386 public synchronized void add(RangeSet that) {
387 int lhs=0,rhs=0;
388 while(lhs<this.ranges.size() && rhs<that.ranges.size()) {
389 Range lr = this.ranges.get(lhs);
390 Range rr = that.ranges.get(rhs);
391
392 // no overlap
393 if(lr.end<rr.start) {
394 lhs++;
395 continue;
396 }
397 if(rr.end<lr.start) {
398 ranges.add(lhs,rr);
399 lhs++;
400 rhs++;
401 continue;
402 }
403
404 // overlap. merge two
405 Range m = lr.combine(rr);
406 rhs++;
407
408 // since ranges[lhs] is expanded, it might overlap with others in this.ranges
409 while(lhs+1<this.ranges.size() && !m.isIndependent(this.ranges.get(lhs+1))) {
410 m = m.combine(this.ranges.get(lhs+1));
411 this.ranges.remove(lhs+1);
412 }
413
414 this.ranges.set(lhs,m);
415 }
416
417 // if anything is left in that.ranges, add them all
418 this.ranges.addAll(that.ranges.subList(rhs,that.ranges.size()));
419 }
420
421 @Override
422 public synchronized String toString() {
423 StringBuilder buf = new StringBuilder();
424 for (Range r : ranges) {
425 if(buf.length()>0) buf.append(',');
426 buf.append(r);
427 }
428 return buf.toString();
429 }
430
431 public synchronized boolean isEmpty() {
432 return ranges.isEmpty();
433 }
434
435 /**
436 * Returns the smallest value in this range.
437 * <p>
438 * If this range is empty, this method throws an exception.
439 */
440 public synchronized int min() {
441 return ranges.get(0).start;
442 }
443
444 /**
445 * Returns the largest value in this range.
446 * <p>
447 * If this range is empty, this method throws an exception.
448 */
449 public synchronized int max() {
450 return ranges.get(ranges.size()-1).end;
451 }
452
453 /**
454 * Returns true if all the integers logically in this {@link RangeSet}
455 * is smaller than the given integer. For example, {[1,3)} is smaller than 3,
456 * but {[1,3),[100,105)} is not smaller than anything less than 105.
457 *
458 * Note that {} is smaller than any n.
459 */
460 public synchronized boolean isSmallerThan(int n) {
461 if(ranges.isEmpty()) return true;
462
463 return ranges.get(ranges.size() - 1).isSmallerThan(n);
464 }
465
466 /**
467 * Parses a {@link RangeSet} from a string like "1-3,5,7-9"
468 */
469 public static RangeSet fromString(String list, boolean skipError) {
470 RangeSet rs = new RangeSet();
471 for (String s : Util.tokenize(list,",")) {
472 s = s.trim();
473 // s is either single number or range "x-y".
474 // note that the end range is inclusive in this notation, but not in the Range class
475 try {
476 if(s.contains("-")) {
477 String[] tokens = Util.tokenize(s,"-");
478 rs.ranges.add(new Range(Integer.parseInt(tokens[0]),Integer.parseInt(tokens[1])+1));
479 } else {
480 int n = Integer.parseInt(s);
481 rs.ranges.add(new Range(n,n+1));
482 }
483 } catch (NumberFormatException e) {
484 if (!skipError)
485 throw new IllegalArgumentException("Unable to parse "+list);
486 // ignore malformed text
487
488 }
489 }
490 return rs;
491 }
492
493 static final class ConverterImpl implements Converter {
494 private final Converter collectionConv; // used to convert ArrayList in it
495
496 public ConverterImpl(Converter collectionConv) {
497 this.collectionConv = collectionConv;
498 }
499
500 public boolean canConvert(Class type) {
501 return type==RangeSet.class;
502 }
503
504 public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
505 RangeSet src = (RangeSet) source;
506
507 StringBuilder buf = new StringBuilder(src.ranges.size()*10);
508 for (Range r : src.ranges) {
509 if(buf.length()>0) buf.append(',');
510 if(r.isSingle())
511 buf.append(r.start);
512 else
513 buf.append(r.start).append('-').append(r.end-1);
514 }
515 writer.setValue(buf.toString());
516 }
517
518 public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
519 if(reader.hasMoreChildren()) {
520 /* old format where <range> elements are nested like
521 <range>
522 <start>1337</start>
523 <end>1479</end>
524 </range>
525 */
526 return new RangeSet((List<Range>)(collectionConv.unmarshal(reader,context)));
527 } else {
528 return RangeSet.fromString(reader.getValue(),true);
529 }
530 }
531 }
532 }
533
534 @Extension
535 public static final class ProjectRenameListener extends ItemListener {
536 @Override
537 public void onRenamed(Item item, String oldName, String newName) {
538 if (item instanceof AbstractProject) {
539 AbstractProject p = Hudson.getInstance().getItemByFullName(newName, AbstractProject.class);
540 if (p != null) {
541 RunList builds = p.getBuilds();
542 for (Object build : builds) {
543 if (build instanceof AbstractBuild) {
544 Collection<Fingerprint> fingerprints = ((AbstractBuild)build).getBuildFingerprints();
545 for (Fingerprint f : fingerprints) {
546 try {
547 f.rename(oldName, newName);
548 } catch (IOException e) {
549 logger.log(Level.WARNING, "Failed to update fingerprint record " + f.getFileName() + " when " + oldName + " was renamed to " + newName, e);
550 }
551 }
552 }
553 }
554 }
555 }
556 }
557 }
558
559 private final Date timestamp;
560
561 /**
562 * Null if this fingerprint is for a file that's
563 * apparently produced outside.
564 */
565 private final BuildPtr original;
566
567 private final byte[] md5sum;
568
569 private final String fileName;
570
571 /**
572 * Range of builds that use this file keyed by a job full name.
573 */
574 private final Hashtable<String,RangeSet> usages = new Hashtable<String,RangeSet>();
575
576 private PersistedList<FingerprintFacet> facets = new PersistedList<FingerprintFacet>(this);
577
578 /**
579 * Lazily computed immutable {@link FingerprintFacet}s created from {@link TransientFingerprintFacetFactory}.
580 */
581 private transient volatile List<FingerprintFacet> transientFacets = null;
582
583 public Fingerprint(Run build, String fileName, byte[] md5sum) throws IOException {
584 this.original = build==null ? null : new BuildPtr(build);
585 this.md5sum = md5sum;
586 this.fileName = fileName;
587 this.timestamp = new Date();
588 save();
589 }
590
591 /**
592 * The first build in which this file showed up,
593 * if the file looked like it's created there.
594 * <p>
595 * This is considered as the "source" of this file,
596 * or the owner, in the sense that this project "owns"
597 * this file.
598 *
599 * @return null
600 * if the file is apparently created outside Hudson.
601 */
602 @Exported
603 public BuildPtr getOriginal() {
604 return original;
605 }
606
607 public String getDisplayName() {
608 return fileName;
609 }
610
611 /**
612 * The file name (like "foo.jar" without path).
613 */
614 @Exported
615 public String getFileName() {
616 return fileName;
617 }
618
619 /**
620 * Gets the MD5 hash string.
621 */
622 @Exported(name="hash")
623 public String getHashString() {
624 return Util.toHexString(md5sum);
625 }
626
627 /**
628 * Gets the timestamp when this record is created.
629 */
630 @Exported
631 public Date getTimestamp() {
632 return timestamp;
633 }
634
635 /**
636 * Gets the string that says how long since this build has scheduled.
637 *
638 * @return
639 * string like "3 minutes" "1 day" etc.
640 */
641 public String getTimestampString() {
642 long duration = System.currentTimeMillis()-timestamp.getTime();
643 return Util.getPastTimeString(duration);
644 }
645
646 /**
647 * Gets the build range set for the given job name.
648 *
649 * <p>
650 * These builds of this job has used this file.
651 */
652 public RangeSet getRangeSet(String jobFullName) {
653 RangeSet r = usages.get(jobFullName);
654 if(r==null) r = new RangeSet();
655 return r;
656 }
657
658 public RangeSet getRangeSet(Job job) {
659 return getRangeSet(job.getFullName());
660 }
661
662 /**
663 * Gets the sorted list of job names where this jar is used.
664 */
665 public List<String> getJobs() {
666 List<String> r = new ArrayList<String>();
667 r.addAll(usages.keySet());
668 Collections.sort(r);
669 return r;
670 }
671
672 public Hashtable<String,RangeSet> getUsages() {
673 return usages;
674 }
675
676 @ExportedBean(defaultVisibility=2)
677 public static final class RangeItem {
678 @Exported
679 public final String name;
680 @Exported
681 public final RangeSet ranges;
682
683 public RangeItem(String name, RangeSet ranges) {
684 this.name = name;
685 this.ranges = ranges;
686 }
687 }
688
689 // this is for remote API
690 @Exported(name="usage")
691 public List<RangeItem> _getUsages() {
692 List<RangeItem> r = new ArrayList<RangeItem>();
693 for (Entry<String, RangeSet> e : usages.entrySet())
694 r.add(new RangeItem(e.getKey(),e.getValue()));
695 return r;
696 }
697
698 public synchronized void add(AbstractBuild b) throws IOException {
699 add(b.getParent().getFullName(),b.getNumber());
700 }
701
702 /**
703 * Records that a build of a job has used this file.
704 */
705 public synchronized void add(String jobFullName, int n) throws IOException {
706 synchronized(usages) {
707 RangeSet r = usages.get(jobFullName);
708 if(r==null) {
709 r = new RangeSet();
710 usages.put(jobFullName,r);
711 }
712 r.add(n);
713 }
714 save();
715 }
716
717 /**
718 * Returns true if any of the builds recorded in this fingerprint
719 * is still retained.
720 *
721 * <p>
722 * This is used to find out old fingerprint records that can be removed
723 * without losing too much information.
724 */
725 public synchronized boolean isAlive() {
726 if(original!=null && original.isAlive())
727 return true;
728
729 for (Entry<String,RangeSet> e : usages.entrySet()) {
730 Job j = Jenkins.getInstance().getItemByFullName(e.getKey(),Job.class);
731 if(j==null)
732 continue;
733
734 Run firstBuild = j.getFirstBuild();
735 if(firstBuild==null)
736 continue;
737
738 int oldest = firstBuild.getNumber();
739 if(!e.getValue().isSmallerThan(oldest))
740 return true;
741 }
742 return false;
743 }
744
745 /**
746 * Gets the associated {@link FingerprintFacet}s.
747 *
748 * <p>
749 * This method always return a non-empty collection, which is a synthetic collection.
750 * It contains persisted {@link FingerprintFacet}s (those that are added explicitly, like
751 * {@code fingerprint.getFacets().add(x)}), as well those {@linkplain TransientFingerprintFacetFactory that are transient}.
752 *
753 * <p>
754 * Mutation to this collection will manipulate persisted set of {@link FingerprintFacet}s, and therefore regardless
755 * of what you do, this collection will always contain a set of {@link FingerprintFacet}s that are added
756 * by {@link TransientFingerprintFacetFactory}s.
757 *
758 * @since 1.421
759 */
760 public Collection<FingerprintFacet> getFacets() {
761 if (transientFacets==null) {
762 List<FingerprintFacet> transientFacets = new ArrayList<FingerprintFacet>();
763 for (TransientFingerprintFacetFactory fff : TransientFingerprintFacetFactory.all()) {
764 fff.createFor(this,transientFacets);
765 }
766 this.transientFacets = ImmutableList.copyOf(transientFacets);
767 }
768
769 return new AbstractCollection<FingerprintFacet>() {
770 @Override
771 public Iterator<FingerprintFacet> iterator() {
772 return Iterators.sequence(facets.iterator(), transientFacets.iterator());
773 }
774
775 @Override
776 public boolean add(FingerprintFacet e) {
777 try {
778 facets.add(e);
779 return true;
780 } catch (IOException x) {
781 throw new Error(x);
782 }
783 }
784
785 @Override
786 public boolean remove(Object o) {
787 try {
788 return facets.remove((FingerprintFacet)o);
789 } catch (IOException x) {
790 throw new Error(x);
791 }
792 }
793
794 @Override
795 public boolean contains(Object o) {
796 return facets.contains(o) || transientFacets.contains(o);
797 }
798
799 @Override
800 public int size() {
801 return facets.size()+transientFacets.size();
802 }
803 };
804 }
805
806 public Collection<FingerprintFacet> getSortedFacets() {
807 List<FingerprintFacet> r = new ArrayList<FingerprintFacet>(getFacets());
808 Collections.sort(r,new Comparator<FingerprintFacet>() {
809 public int compare(FingerprintFacet o1, FingerprintFacet o2) {
810 long a = o1.getTimestamp();
811 long b = o2.getTimestamp();
812 if (a<b) return -1;
813 if (a==b) return 0;
814 return 1;
815 }
816 });
817 return r;
818 }
819
820 /**
821 * Returns the actions contributed from {@link #getFacets()}
822 */
823 public List<Action> getActions() {
824 List<Action> r = new ArrayList<Action>();
825 for (FingerprintFacet ff : getFacets())
826 ff.createActions(r);
827 return Collections.unmodifiableList(r);
828 }
829
830 /**
831 * Save the settings to a file.
832 */
833 public synchronized void save() throws IOException {
834 if(BulkChange.contains(this)) return;
835
836 long start=0;
837 if(logger.isLoggable(Level.FINE))
838 start = System.currentTimeMillis();
839
840 File file = getFingerprintFile(md5sum);
841 getConfigFile(file).write(this);
842 SaveableListener.fireOnChange(this, getConfigFile(file));
843
844 if(logger.isLoggable(Level.FINE))
845 logger.fine("Saving fingerprint "+file+" took "+(System.currentTimeMillis()-start)+"ms");
846 }
847
848 /**
849 * Update references to a renamed job in the fingerprint
850 */
851 public synchronized void rename(String oldName, String newName) throws IOException {
852 boolean touched = false;
853 if (original != null) {
854 if (original.getName().equals(oldName)) {
855 original.setName(newName);
856 touched = true;
857 }
858 }
859
860 if (usages != null) {
861 RangeSet r = usages.get(oldName);
862 if (r != null) {
863 usages.put(newName, r);
864 usages.remove(oldName);
865 touched = true;
866 }
867 }
868
869 if (touched) {
870 save();
871 }
872 }
873
874 public Api getApi() {
875 return new Api(this);
876 }
877
878 /**
879 * The file we save our configuration.
880 */
881 private static XmlFile getConfigFile(File file) {
882 return new XmlFile(XSTREAM,file);
883 }
884
885 /**
886 * Determines the file name from md5sum.
887 */
888 private static File getFingerprintFile(byte[] md5sum) {
889 assert md5sum.length==16;
890 return new File( Jenkins.getInstance().getRootDir(),
891 "fingerprints/"+ Util.toHexString(md5sum,0,1)+'/'+Util.toHexString(md5sum,1,1)+'/'+Util.toHexString(md5sum,2,md5sum.length-2)+".xml");
892 }
893
894 /**
895 * Loads a {@link Fingerprint} from a file in the image.
896 */
897 /*package*/ static Fingerprint load(byte[] md5sum) throws IOException {
898 return load(getFingerprintFile(md5sum));
899 }
900 /*package*/ static Fingerprint load(File file) throws IOException {
901 XmlFile configFile = getConfigFile(file);
902 if(!configFile.exists())
903 return null;
904
905 long start=0;
906 if(logger.isLoggable(Level.FINE))
907 start = System.currentTimeMillis();
908
909 try {
910 Fingerprint f = (Fingerprint) configFile.read();
911 if(logger.isLoggable(Level.FINE))
912 logger.fine("Loading fingerprint "+file+" took "+(System.currentTimeMillis()-start)+"ms");
913 if (f.facets==null)
914 f.facets = new PersistedList<FingerprintFacet>(f);
915 for (FingerprintFacet facet : f.facets)
916 facet._setOwner(f);
917 return f;
918 } catch (IOException e) {
919 if(file.exists() && file.length()==0) {
920 // Despite the use of AtomicFile, there are reports indicating that people often see
921 // empty XML file, presumably either due to file system corruption (perhaps by sudden
922 // power loss, etc.) or abnormal program termination.
923 // generally we don't want to wipe out user data just because we can't load it,
924 // but if the file size is 0, which is what's reported in HUDSON-2012, then it seems
925 // like recovering it silently by deleting the file is not a bad idea.
926 logger.log(Level.WARNING, "Size zero fingerprint. Disk corruption? "+configFile,e);
927 file.delete();
928 return null;
929 }
930 logger.log(Level.WARNING, "Failed to load "+configFile,e);
931 throw e;
932 }
933 }
934
935 private static final XStream XSTREAM = new XStream2();
936 static {
937 XSTREAM.alias("fingerprint",Fingerprint.class);
938 XSTREAM.alias("range",Range.class);
939 XSTREAM.alias("ranges",RangeSet.class);
940 XSTREAM.registerConverter(new HexBinaryConverter(),10);
941 XSTREAM.registerConverter(new RangeSet.ConverterImpl(
942 new CollectionConverter(XSTREAM.getMapper()) {
943 @Override
944 protected Object createCollection(Class type) {
945 return new ArrayList();
946 }
947 }
948 ),10);
949 }
950
951 private static final Logger logger = Logger.getLogger(Fingerprint.class.getName());
952 }
953