/* * Copyright (C) 2010, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.util; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.BinaryOperator; import java.util.stream.Collector; import java.util.stream.Collectors; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefComparator; /** * Specialized Map to present a {@code RefDatabase} namespace. *

* Although not declared as a {@link java.util.SortedMap}, iterators from this * map's projections always return references in * {@link org.eclipse.jgit.lib.RefComparator} ordering. The map's internal * representation is a sorted array of {@link org.eclipse.jgit.lib.Ref} objects, * which means lookup and replacement is O(log N), while insertion and removal * can be as expensive as O(N + log N) while the list expands or contracts. * Since this is not a general map implementation, all entries must be keyed by * the reference name. *

* This class is really intended as a helper for {@code RefDatabase}, which * needs to perform a merge-join of three sorted * {@link org.eclipse.jgit.util.RefList}s in order to present the unified * namespace of the packed-refs file, the loose refs/ directory tree, and the * resolved form of any symbolic references. */ public class RefMap extends AbstractMap { /** * Prefix denoting the reference subspace this map contains. *

* All reference names in this map must start with this prefix. If the * prefix is not the empty string, it must end with a '/'. */ final String prefix; /** Immutable collection of the packed references at construction time. */ RefList packed; /** * Immutable collection of the loose references at construction time. *

* If an entry appears here and in {@link #packed}, this entry must take * precedence, as its more current. Symbolic references in this collection * are typically unresolved, so they only tell us who their target is, but * not the current value of the target. */ RefList loose; /** * Immutable collection of resolved symbolic references. *

* This collection contains only the symbolic references we were able to * resolve at map construction time. Other loose references must be read * from {@link #loose}. Every entry in this list must be matched by an entry * in {@code loose}, otherwise it might be omitted by the map. */ RefList resolved; int size; boolean sizeIsValid; private Set> entrySet; /** * Construct an empty map with a small initial capacity. */ public RefMap() { prefix = ""; //$NON-NLS-1$ packed = RefList.emptyList(); loose = RefList.emptyList(); resolved = RefList.emptyList(); } /** * Construct a map to merge 3 collections together. * * @param prefix * prefix used to slice the lists down. Only references whose * names start with this prefix will appear to reside in the map. * Must not be null, use {@code ""} (the empty string) to select * all list items. * @param packed * items from the packed reference list, this is the last list * searched. * @param loose * items from the loose reference list, this list overrides * {@code packed} if a name appears in both. * @param resolved * resolved symbolic references. This list overrides the prior * list {@code loose}, if an item appears in both. Items in this * list must also appear in {@code loose}. */ @SuppressWarnings("unchecked") public RefMap(String prefix, RefList packed, RefList loose, RefList resolved) { this.prefix = prefix; this.packed = (RefList) packed; this.loose = (RefList) loose; this.resolved = (RefList) resolved; } /** {@inheritDoc} */ @Override public boolean containsKey(Object name) { return get(name) != null; } /** {@inheritDoc} */ @Override public Ref get(Object key) { String name = toRefName((String) key); Ref ref = resolved.get(name); if (ref == null) ref = loose.get(name); if (ref == null) ref = packed.get(name); return ref; } /** {@inheritDoc} */ @Override public Ref put(String keyName, Ref value) { String name = toRefName(keyName); if (!name.equals(value.getName())) throw new IllegalArgumentException(); if (!resolved.isEmpty()) { // Collapse the resolved list into the loose list so we // can discard it and stop joining the two together. for (Ref ref : resolved) loose = loose.put(ref); resolved = RefList.emptyList(); } int idx = loose.find(name); if (0 <= idx) { Ref prior = loose.get(name); loose = loose.set(idx, value); return prior; } Ref prior = get(keyName); loose = loose.add(idx, value); sizeIsValid = false; return prior; } /** {@inheritDoc} */ @Override public Ref remove(Object key) { String name = toRefName((String) key); Ref res = null; int idx; if (0 <= (idx = packed.find(name))) { res = packed.get(name); packed = packed.remove(idx); sizeIsValid = false; } if (0 <= (idx = loose.find(name))) { res = loose.get(name); loose = loose.remove(idx); sizeIsValid = false; } if (0 <= (idx = resolved.find(name))) { res = resolved.get(name); resolved = resolved.remove(idx); sizeIsValid = false; } return res; } /** {@inheritDoc} */ @Override public boolean isEmpty() { return entrySet().isEmpty(); } /** {@inheritDoc} */ @Override public Set> entrySet() { if (entrySet == null) { entrySet = new AbstractSet<>() { @Override public Iterator> iterator() { return new SetIterator(); } @Override public int size() { if (!sizeIsValid) { size = 0; Iterator i = entrySet().iterator(); for (; i.hasNext(); i.next()) size++; sizeIsValid = true; } return size; } @Override public boolean isEmpty() { if (sizeIsValid) return 0 == size; return !iterator().hasNext(); } @Override public void clear() { packed = RefList.emptyList(); loose = RefList.emptyList(); resolved = RefList.emptyList(); size = 0; sizeIsValid = true; } }; } return entrySet; } /** {@inheritDoc} */ @Override public String toString() { StringBuilder r = new StringBuilder(); boolean first = true; r.append('['); for (Ref ref : values()) { if (first) first = false; else r.append(", "); //$NON-NLS-1$ r.append(ref); } r.append(']'); return r.toString(); } /** * Create a {@link Collector} for {@link Ref}. * * @param mergeFunction * @return {@link Collector} for {@link Ref} * @since 5.4 */ public static Collector toRefMap( BinaryOperator mergeFunction) { return Collectors.collectingAndThen(RefList.toRefList(mergeFunction), (refs) -> new RefMap("", //$NON-NLS-1$ refs, RefList.emptyList(), RefList.emptyList())); } private String toRefName(String name) { if (0 < prefix.length()) name = prefix + name; return name; } String toMapKey(Ref ref) { String name = ref.getName(); if (0 < prefix.length()) name = name.substring(prefix.length()); return name; } private class SetIterator implements Iterator> { private int packedIdx; private int looseIdx; private int resolvedIdx; private Entry next; SetIterator() { if (0 < prefix.length()) { packedIdx = -(packed.find(prefix) + 1); looseIdx = -(loose.find(prefix) + 1); resolvedIdx = -(resolved.find(prefix) + 1); } } @Override public boolean hasNext() { if (next == null) next = peek(); return next != null; } @Override public Entry next() { if (hasNext()) { Entry r = next; next = peek(); return r; } throw new NoSuchElementException(); } public Entry peek() { if (packedIdx < packed.size() && looseIdx < loose.size()) { Ref p = packed.get(packedIdx); Ref l = loose.get(looseIdx); int cmp = RefComparator.compareTo(p, l); if (cmp < 0) { packedIdx++; return toEntry(p); } if (cmp == 0) packedIdx++; looseIdx++; return toEntry(resolveLoose(l)); } if (looseIdx < loose.size()) return toEntry(resolveLoose(loose.get(looseIdx++))); if (packedIdx < packed.size()) return toEntry(packed.get(packedIdx++)); return null; } private Ref resolveLoose(Ref l) { if (resolvedIdx < resolved.size()) { Ref r = resolved.get(resolvedIdx); int cmp = RefComparator.compareTo(l, r); if (cmp == 0) { resolvedIdx++; return r; } else if (cmp > 0) { // WTF, we have a symbolic entry but no match // in the loose collection. That's an error. throw new IllegalStateException(); } } return l; } private Ent toEntry(Ref p) { if (p.getName().startsWith(prefix)) return new Ent(p); packedIdx = packed.size(); looseIdx = loose.size(); resolvedIdx = resolved.size(); return null; } @Override public void remove() { throw new UnsupportedOperationException(); } } private class Ent implements Entry { private Ref ref; Ent(Ref ref) { this.ref = ref; } @Override public String getKey() { return toMapKey(ref); } @Override public Ref getValue() { return ref; } @Override public Ref setValue(Ref value) { Ref prior = put(getKey(), value); ref = value; return prior; } @Override public int hashCode() { return getKey().hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof Map.Entry) { final Object key = ((Map.Entry) obj).getKey(); final Object val = ((Map.Entry) obj).getValue(); if (key instanceof String && val instanceof Ref) { final Ref r = (Ref) val; if (r.getName().equals(ref.getName())) { final ObjectId a = r.getObjectId(); final ObjectId b = ref.getObjectId(); if (a != null && b != null && AnyObjectId.isEqual(a, b)) { return true; } } } } return false; } @Override public String toString() { return ref.toString(); } } }