View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.util;
12  
13  import java.util.AbstractMap;
14  import java.util.AbstractSet;
15  import java.util.Iterator;
16  import java.util.Map;
17  import java.util.NoSuchElementException;
18  import java.util.Set;
19  import java.util.function.BinaryOperator;
20  import java.util.stream.Collector;
21  import java.util.stream.Collectors;
22  
23  import org.eclipse.jgit.lib.AnyObjectId;
24  import org.eclipse.jgit.lib.ObjectId;
25  import org.eclipse.jgit.lib.Ref;
26  import org.eclipse.jgit.lib.RefComparator;
27  
28  /**
29   * Specialized Map to present a {@code RefDatabase} namespace.
30   * <p>
31   * Although not declared as a {@link java.util.SortedMap}, iterators from this
32   * map's projections always return references in
33   * {@link org.eclipse.jgit.lib.RefComparator} ordering. The map's internal
34   * representation is a sorted array of {@link org.eclipse.jgit.lib.Ref} objects,
35   * which means lookup and replacement is O(log N), while insertion and removal
36   * can be as expensive as O(N + log N) while the list expands or contracts.
37   * Since this is not a general map implementation, all entries must be keyed by
38   * the reference name.
39   * <p>
40   * This class is really intended as a helper for {@code RefDatabase}, which
41   * needs to perform a merge-join of three sorted
42   * {@link org.eclipse.jgit.util.RefList}s in order to present the unified
43   * namespace of the packed-refs file, the loose refs/ directory tree, and the
44   * resolved form of any symbolic references.
45   */
46  public class RefMap extends AbstractMap<String, Ref> {
47  	/**
48  	 * Prefix denoting the reference subspace this map contains.
49  	 * <p>
50  	 * All reference names in this map must start with this prefix. If the
51  	 * prefix is not the empty string, it must end with a '/'.
52  	 */
53  	final String prefix;
54  
55  	/** Immutable collection of the packed references at construction time. */
56  	RefList<Ref> packed;
57  
58  	/**
59  	 * Immutable collection of the loose references at construction time.
60  	 * <p>
61  	 * If an entry appears here and in {@link #packed}, this entry must take
62  	 * precedence, as its more current. Symbolic references in this collection
63  	 * are typically unresolved, so they only tell us who their target is, but
64  	 * not the current value of the target.
65  	 */
66  	RefList<Ref> loose;
67  
68  	/**
69  	 * Immutable collection of resolved symbolic references.
70  	 * <p>
71  	 * This collection contains only the symbolic references we were able to
72  	 * resolve at map construction time. Other loose references must be read
73  	 * from {@link #loose}. Every entry in this list must be matched by an entry
74  	 * in {@code loose}, otherwise it might be omitted by the map.
75  	 */
76  	RefList<Ref> resolved;
77  
78  	int size;
79  
80  	boolean sizeIsValid;
81  
82  	private Set<Entry<String, Ref>> entrySet;
83  
84  	/**
85  	 * Construct an empty map with a small initial capacity.
86  	 */
87  	public RefMap() {
88  		prefix = ""; //$NON-NLS-1$
89  		packed = RefList.emptyList();
90  		loose = RefList.emptyList();
91  		resolved = RefList.emptyList();
92  	}
93  
94  	/**
95  	 * Construct a map to merge 3 collections together.
96  	 *
97  	 * @param prefix
98  	 *            prefix used to slice the lists down. Only references whose
99  	 *            names start with this prefix will appear to reside in the map.
100 	 *            Must not be null, use {@code ""} (the empty string) to select
101 	 *            all list items.
102 	 * @param packed
103 	 *            items from the packed reference list, this is the last list
104 	 *            searched.
105 	 * @param loose
106 	 *            items from the loose reference list, this list overrides
107 	 *            {@code packed} if a name appears in both.
108 	 * @param resolved
109 	 *            resolved symbolic references. This list overrides the prior
110 	 *            list {@code loose}, if an item appears in both. Items in this
111 	 *            list <b>must</b> also appear in {@code loose}.
112 	 */
113 	@SuppressWarnings("unchecked")
114 	public RefMap(String prefix, RefList<? extends Ref> packed,
115 			RefList<? extends Ref> loose, RefList<? extends Ref> resolved) {
116 		this.prefix = prefix;
117 		this.packed = (RefList<Ref>) packed;
118 		this.loose = (RefList<Ref>) loose;
119 		this.resolved = (RefList<Ref>) resolved;
120 	}
121 
122 	/** {@inheritDoc} */
123 	@Override
124 	public boolean containsKey(Object name) {
125 		return get(name) != null;
126 	}
127 
128 	/** {@inheritDoc} */
129 	@Override
130 	public Ref get(Object key) {
131 		String name = toRefName((String) key);
132 		Ref ref = resolved.get(name);
133 		if (ref == null)
134 			ref = loose.get(name);
135 		if (ref == null)
136 			ref = packed.get(name);
137 		return ref;
138 	}
139 
140 	/** {@inheritDoc} */
141 	@Override
142 	public Ref./../org/eclipse/jgit/lib/Ref.html#Ref">Ref put(String keyName, Ref value) {
143 		String name = toRefName(keyName);
144 
145 		if (!name.equals(value.getName()))
146 			throw new IllegalArgumentException();
147 
148 		if (!resolved.isEmpty()) {
149 			// Collapse the resolved list into the loose list so we
150 			// can discard it and stop joining the two together.
151 			for (Ref ref : resolved)
152 				loose = loose.put(ref);
153 			resolved = RefList.emptyList();
154 		}
155 
156 		int idx = loose.find(name);
157 		if (0 <= idx) {
158 			Ref prior = loose.get(name);
159 			loose = loose.set(idx, value);
160 			return prior;
161 		}
162 		Ref prior = get(keyName);
163 		loose = loose.add(idx, value);
164 		sizeIsValid = false;
165 		return prior;
166 	}
167 
168 	/** {@inheritDoc} */
169 	@Override
170 	public Ref remove(Object key) {
171 		String name = toRefName((String) key);
172 		Ref res = null;
173 		int idx;
174 		if (0 <= (idx = packed.find(name))) {
175 			res = packed.get(name);
176 			packed = packed.remove(idx);
177 			sizeIsValid = false;
178 		}
179 		if (0 <= (idx = loose.find(name))) {
180 			res = loose.get(name);
181 			loose = loose.remove(idx);
182 			sizeIsValid = false;
183 		}
184 		if (0 <= (idx = resolved.find(name))) {
185 			res = resolved.get(name);
186 			resolved = resolved.remove(idx);
187 			sizeIsValid = false;
188 		}
189 		return res;
190 	}
191 
192 	/** {@inheritDoc} */
193 	@Override
194 	public boolean isEmpty() {
195 		return entrySet().isEmpty();
196 	}
197 
198 	/** {@inheritDoc} */
199 	@Override
200 	public Set<Entry<String, Ref>> entrySet() {
201 		if (entrySet == null) {
202 			entrySet = new AbstractSet<Entry<String, Ref>>() {
203 				@Override
204 				public Iterator<Entry<String, Ref>> iterator() {
205 					return new SetIterator();
206 				}
207 
208 				@Override
209 				public int size() {
210 					if (!sizeIsValid) {
211 						size = 0;
212 						Iterator<?> i = entrySet().iterator();
213 						for (; i.hasNext(); i.next())
214 							size++;
215 						sizeIsValid = true;
216 					}
217 					return size;
218 				}
219 
220 				@Override
221 				public boolean isEmpty() {
222 					if (sizeIsValid)
223 						return 0 == size;
224 					return !iterator().hasNext();
225 				}
226 
227 				@Override
228 				public void clear() {
229 					packed = RefList.emptyList();
230 					loose = RefList.emptyList();
231 					resolved = RefList.emptyList();
232 					size = 0;
233 					sizeIsValid = true;
234 				}
235 			};
236 		}
237 		return entrySet;
238 	}
239 
240 	/** {@inheritDoc} */
241 	@Override
242 	public String toString() {
243 		StringBuilder r = new StringBuilder();
244 		boolean first = true;
245 		r.append('[');
246 		for (Ref ref : values()) {
247 			if (first)
248 				first = false;
249 			else
250 				r.append(", "); //$NON-NLS-1$
251 			r.append(ref);
252 		}
253 		r.append(']');
254 		return r.toString();
255 	}
256 
257 	/**
258 	 * Create a {@link Collector} for {@link Ref}.
259 	 *
260 	 * @param mergeFunction
261 	 * @return {@link Collector} for {@link Ref}
262 	 * @since 5.4
263 	 */
264 	public static Collector<Ref, ?, RefMap> toRefMap(
265 			BinaryOperator<Ref> mergeFunction) {
266 		return Collectors.collectingAndThen(RefList.toRefList(mergeFunction),
267 				(refs) -> new RefMap("", //$NON-NLS-1$
268 							refs, RefList.emptyList(),
269 						RefList.emptyList()));
270 	}
271 
272 	private String toRefName(String name) {
273 		if (0 < prefix.length())
274 			name = prefix + name;
275 		return name;
276 	}
277 
278 	String toMapKey(Ref ref) {
279 		String name = ref.getName();
280 		if (0 < prefix.length())
281 			name = name.substring(prefix.length());
282 		return name;
283 	}
284 
285 	private class SetIterator implements Iterator<Entry<String, Ref>> {
286 		private int packedIdx;
287 
288 		private int looseIdx;
289 
290 		private int resolvedIdx;
291 
292 		private Entry<String, Ref> next;
293 
294 		SetIterator() {
295 			if (0 < prefix.length()) {
296 				packedIdx = -(packed.find(prefix) + 1);
297 				looseIdx = -(loose.find(prefix) + 1);
298 				resolvedIdx = -(resolved.find(prefix) + 1);
299 			}
300 		}
301 
302 		@Override
303 		public boolean hasNext() {
304 			if (next == null)
305 				next = peek();
306 			return next != null;
307 		}
308 
309 		@Override
310 		public Entry<String, Ref> next() {
311 			if (hasNext()) {
312 				Entry<String, Ref> r = next;
313 				next = peek();
314 				return r;
315 			}
316 			throw new NoSuchElementException();
317 		}
318 
319 		public Entry<String, Ref> peek() {
320 			if (packedIdx < packed.size() && looseIdx < loose.size()) {
321 				Ref p = packed.get(packedIdx);
322 				Ref l = loose.get(looseIdx);
323 				int cmp = RefComparator.compareTo(p, l);
324 				if (cmp < 0) {
325 					packedIdx++;
326 					return toEntry(p);
327 				}
328 
329 				if (cmp == 0)
330 					packedIdx++;
331 				looseIdx++;
332 				return toEntry(resolveLoose(l));
333 			}
334 
335 			if (looseIdx < loose.size())
336 				return toEntry(resolveLoose(loose.get(looseIdx++)));
337 			if (packedIdx < packed.size())
338 				return toEntry(packed.get(packedIdx++));
339 			return null;
340 		}
341 
342 		private Ref../../../../org/eclipse/jgit/lib/Ref.html#Ref">Ref resolveLoose(Ref l) {
343 			if (resolvedIdx < resolved.size()) {
344 				Ref r = resolved.get(resolvedIdx);
345 				int cmp = RefComparator.compareTo(l, r);
346 				if (cmp == 0) {
347 					resolvedIdx++;
348 					return r;
349 				} else if (cmp > 0) {
350 					// WTF, we have a symbolic entry but no match
351 					// in the loose collection. That's an error.
352 					throw new IllegalStateException();
353 				}
354 			}
355 			return l;
356 		}
357 
358 		private Ent toEntry(Ref p) {
359 			if (p.getName().startsWith(prefix))
360 				return new Ent(p);
361 			packedIdx = packed.size();
362 			looseIdx = loose.size();
363 			resolvedIdx = resolved.size();
364 			return null;
365 		}
366 
367 		@Override
368 		public void remove() {
369 			throw new UnsupportedOperationException();
370 		}
371 	}
372 
373 	private class Ent implements Entry<String, Ref> {
374 		private Ref ref;
375 
376 		Ent(Ref ref) {
377 			this.ref = ref;
378 		}
379 
380 		@Override
381 		public String getKey() {
382 			return toMapKey(ref);
383 		}
384 
385 		@Override
386 		public Ref getValue() {
387 			return ref;
388 		}
389 
390 		@Override
391 		public Refef="../../../../org/eclipse/jgit/lib/Ref.html#Ref">Ref setValue(Ref value) {
392 			Ref prior = put(getKey(), value);
393 			ref = value;
394 			return prior;
395 		}
396 
397 		@Override
398 		public int hashCode() {
399 			return getKey().hashCode();
400 		}
401 
402 		@Override
403 		public boolean equals(Object obj) {
404 			if (obj instanceof Map.Entry) {
405 				final Object key = ((Map.Entry) obj).getKey();
406 				final Object val = ((Map.Entry) obj).getValue();
407 				if (key instanceof String && val instanceof Ref) {
408 					final Ref" href="../../../../org/eclipse/jgit/lib/Ref.html#Ref">Ref r = (Ref) val;
409 					if (r.getName().equals(ref.getName())) {
410 						final ObjectId a = r.getObjectId();
411 						final ObjectId b = ref.getObjectId();
412 						if (a != null && b != null
413 								&& AnyObjectId.isEqual(a, b)) {
414 							return true;
415 						}
416 					}
417 				}
418 			}
419 			return false;
420 		}
421 
422 		@Override
423 		public String toString() {
424 			return ref.toString();
425 		}
426 	}
427 }