View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.notes;
45  
46  import static org.eclipse.jgit.lib.FileMode.TREE;
47  
48  import java.io.IOException;
49  import java.util.Iterator;
50  import java.util.NoSuchElementException;
51  
52  import org.eclipse.jgit.lib.AbbreviatedObjectId;
53  import org.eclipse.jgit.lib.AnyObjectId;
54  import org.eclipse.jgit.lib.MutableObjectId;
55  import org.eclipse.jgit.lib.ObjectId;
56  import org.eclipse.jgit.lib.ObjectInserter;
57  import org.eclipse.jgit.lib.ObjectReader;
58  import org.eclipse.jgit.lib.TreeFormatter;
59  
60  /**
61   * A note tree holding only note subtrees, each named using a 2 digit hex name.
62   *
63   * The fanout buckets/trees contain on average 256 subtrees, naming the subtrees
64   * by a slice of the ObjectId contained within them, from "00" through "ff".
65   *
66   * Each fanout bucket has a {@link #prefixLen} that defines how many digits it
67   * skips in an ObjectId before it gets to the digits matching {@link #table}.
68   *
69   * The root tree has {@code prefixLen == 0}, and thus does not skip any digits.
70   * For ObjectId "c0ffee...", the note (if it exists) will be stored within the
71   * bucket {@code table[0xc0]}.
72   *
73   * The first level tree has {@code prefixLen == 2}, and thus skips the first two
74   * digits. For the same example "c0ffee..." object, its note would be found
75   * within the {@code table[0xff]} bucket (as first 2 digits "c0" are skipped).
76   *
77   * Each subtree is loaded on-demand, reducing startup latency for reads that
78   * only need to examine a few objects. However, due to the rather uniform
79   * distribution of the SHA-1 hash that is used for ObjectIds, accessing 256
80   * objects is very likely to load all of the subtrees into memory.
81   *
82   * A FanoutBucket must be parsed from a tree object by {@link NoteParser}.
83   */
84  class FanoutBucket extends InMemoryNoteBucket {
85  	/**
86  	 * Fan-out table similar to the PackIndex structure.
87  	 *
88  	 * Notes for an object are stored within the sub-bucket that is held here as
89  	 * {@code table[ objectId.getByte( prefixLen / 2 ) ]}. If the slot is null
90  	 * there are no notes with that prefix.
91  	 */
92  	private final NoteBucket[] table;
93  
94  	/** Number of non-null slots in {@link #table}. */
95  	private int cnt;
96  
97  	FanoutBucket(int prefixLen) {
98  		super(prefixLen);
99  		table = new NoteBucket[256];
100 	}
101 
102 	void setBucket(int cell, ObjectId id) {
103 		table[cell] = new LazyNoteBucket(id);
104 		cnt++;
105 	}
106 
107 	void setBucket(int cell, InMemoryNoteBucket bucket) {
108 		table[cell] = bucket;
109 		cnt++;
110 	}
111 
112 	@Override
113 	Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
114 		NoteBucket b = table[cell(objId)];
115 		return b != null ? b.getNote(objId, or) : null;
116 
117 	}
118 
119 	NoteBucket getBucket(int cell) {
120 		return table[cell];
121 	}
122 
123 	static InMemoryNoteBucket loadIfLazy(NoteBucket b, AnyObjectId prefix,
124 			ObjectReader or) throws IOException {
125 		if (b == null)
126 			return null;
127 		if (b instanceof InMemoryNoteBucket)
128 			return (InMemoryNoteBucket) b;
129 		return ((LazyNoteBucket) b).load(prefix, or);
130 	}
131 
132 	@Override
133 	Iterator<Note> iterator(AnyObjectId objId, final ObjectReader reader)
134 			throws IOException {
135 		final MutableObjectId id = new MutableObjectId();
136 		id.fromObjectId(objId);
137 
138 		return new Iterator<Note>() {
139 			private int cell;
140 
141 			private Iterator<Note> itr;
142 
143 			public boolean hasNext() {
144 				if (itr != null && itr.hasNext())
145 					return true;
146 
147 				for (; cell < table.length; cell++) {
148 					NoteBucket b = table[cell];
149 					if (b == null)
150 						continue;
151 
152 					try {
153 						id.setByte(prefixLen >> 1, cell);
154 						itr = b.iterator(id, reader);
155 					} catch (IOException err) {
156 						throw new RuntimeException(err);
157 					}
158 
159 					if (itr.hasNext()) {
160 						cell++;
161 						return true;
162 					}
163 				}
164 				return false;
165 			}
166 
167 			public Note next() {
168 				if (hasNext())
169 					return itr.next();
170 				else
171 					throw new NoSuchElementException();
172 			}
173 
174 			public void remove() {
175 				throw new UnsupportedOperationException();
176 			}
177 		};
178 	}
179 
180 	@Override
181 	int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
182 		// If most of this fan-out is full, estimate it should still be split.
183 		if (LeafBucket.MAX_SIZE * 3 / 4 <= cnt)
184 			return 1 + LeafBucket.MAX_SIZE;
185 
186 		// Due to the uniform distribution of ObjectIds, having less nodes full
187 		// indicates a good chance the total number of children below here
188 		// is less than the MAX_SIZE split point. Get a more accurate count.
189 
190 		MutableObjectId id = new MutableObjectId();
191 		id.fromObjectId(noteOn);
192 
193 		int sz = 0;
194 		for (int cell = 0; cell < 256; cell++) {
195 			NoteBucket b = table[cell];
196 			if (b == null)
197 				continue;
198 
199 			id.setByte(prefixLen >> 1, cell);
200 			sz += b.estimateSize(id, or);
201 			if (LeafBucket.MAX_SIZE < sz)
202 				break;
203 		}
204 		return sz;
205 	}
206 
207 	@Override
208 	InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
209 			ObjectReader or) throws IOException {
210 		int cell = cell(noteOn);
211 		NoteBucket b = table[cell];
212 
213 		if (b == null) {
214 			if (noteData == null)
215 				return this;
216 
217 			LeafBucket n = new LeafBucket(prefixLen + 2);
218 			table[cell] = n.set(noteOn, noteData, or);
219 			cnt++;
220 			return this;
221 
222 		} else {
223 			NoteBucket n = b.set(noteOn, noteData, or);
224 			if (n == null) {
225 				table[cell] = null;
226 				cnt--;
227 
228 				if (cnt == 0)
229 					return null;
230 
231 				return contractIfTooSmall(noteOn, or);
232 
233 			} else if (n != b) {
234 				table[cell] = n;
235 			}
236 			return this;
237 		}
238 	}
239 
240 	InMemoryNoteBucket contractIfTooSmall(AnyObjectId noteOn, ObjectReader or)
241 			throws IOException {
242 		if (estimateSize(noteOn, or) < LeafBucket.MAX_SIZE) {
243 			// We are small enough to just contract to a single leaf.
244 			InMemoryNoteBucket r = new LeafBucket(prefixLen);
245 			for (Iterator<Note> i = iterator(noteOn, or); i.hasNext();)
246 				r = r.append(i.next());
247 			r.nonNotes = nonNotes;
248 			return r;
249 		}
250 
251 		return this;
252 	}
253 
254 	private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6',
255 			'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
256 
257 	@Override
258 	ObjectId writeTree(ObjectInserter inserter) throws IOException {
259 		return inserter.insert(build(true, inserter));
260 	}
261 
262 	ObjectId getTreeId() {
263 		try (ObjectInserter.Formatter f = new ObjectInserter.Formatter()) {
264 			return f.idFor(build(false, null));
265 		} catch (IOException e) {
266 			// should never happen as we are not inserting
267 			throw new RuntimeException(e);
268 		}
269 	}
270 
271 	private TreeFormatter build(boolean insert, ObjectInserter inserter)
272 			throws IOException {
273 		byte[] nameBuf = new byte[2];
274 		TreeFormatter fmt = new TreeFormatter(treeSize());
275 		NonNoteEntry e = nonNotes;
276 
277 		for (int cell = 0; cell < 256; cell++) {
278 			NoteBucket b = table[cell];
279 			if (b == null)
280 				continue;
281 
282 			nameBuf[0] = hexchar[cell >>> 4];
283 			nameBuf[1] = hexchar[cell & 0x0f];
284 
285 			while (e != null && e.pathCompare(nameBuf, 0, 2, TREE) < 0) {
286 				e.format(fmt);
287 				e = e.next;
288 			}
289 
290 			ObjectId id;
291 			if (insert) {
292 				id = b.writeTree(inserter);
293 			} else {
294 				id = b.getTreeId();
295 			}
296 			fmt.append(nameBuf, 0, 2, TREE, id);
297 		}
298 
299 		for (; e != null; e = e.next)
300 			e.format(fmt);
301 		return fmt;
302 	}
303 
304 	private int treeSize() {
305 		int sz = cnt * TreeFormatter.entrySize(TREE, 2);
306 		for (NonNoteEntry e = nonNotes; e != null; e = e.next)
307 			sz += e.treeEntrySize();
308 		return sz;
309 	}
310 
311 	@Override
312 	InMemoryNoteBucket append(Note note) {
313 		int cell = cell(note);
314 		InMemoryNoteBucket b = (InMemoryNoteBucket) table[cell];
315 
316 		if (b == null) {
317 			LeafBucket n = new LeafBucket(prefixLen + 2);
318 			table[cell] = n.append(note);
319 			cnt++;
320 
321 		} else {
322 			InMemoryNoteBucket n = b.append(note);
323 			if (n != b)
324 				table[cell] = n;
325 		}
326 		return this;
327 	}
328 
329 	private int cell(AnyObjectId id) {
330 		return id.getByte(prefixLen >> 1);
331 	}
332 
333 	private class LazyNoteBucket extends NoteBucket {
334 		private final ObjectId treeId;
335 
336 		LazyNoteBucket(ObjectId treeId) {
337 			this.treeId = treeId;
338 		}
339 
340 		@Override
341 		Note getNote(AnyObjectId objId, ObjectReader or) throws IOException {
342 			return load(objId, or).getNote(objId, or);
343 		}
344 
345 		@Override
346 		Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader)
347 				throws IOException {
348 			return load(objId, reader).iterator(objId, reader);
349 		}
350 
351 		@Override
352 		int estimateSize(AnyObjectId objId, ObjectReader or) throws IOException {
353 			return load(objId, or).estimateSize(objId, or);
354 		}
355 
356 		@Override
357 		InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
358 				ObjectReader or) throws IOException {
359 			return load(noteOn, or).set(noteOn, noteData, or);
360 		}
361 
362 		@Override
363 		ObjectId writeTree(ObjectInserter inserter) {
364 			return treeId;
365 		}
366 
367 		@Override
368 		ObjectId getTreeId() {
369 			return treeId;
370 		}
371 
372 		private InMemoryNoteBucket load(AnyObjectId prefix, ObjectReader or)
373 				throws IOException {
374 			AbbreviatedObjectId p = prefix.abbreviate(prefixLen + 2);
375 			InMemoryNoteBucket self = NoteParser.parse(p, treeId, or);
376 			table[cell(prefix)] = self;
377 			return self;
378 		}
379 	}
380 }