From ab697ff18b09e5d49e028b7a32844d408b02ccf2 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Fri, 22 Jan 2010 16:27:03 -0800 Subject: [PATCH] Create new RefList and RefMap utility types These types can be used by RefDatabase implementations to manage the collection. A RefList stores items sorted by their name, and is an immutable type using copy-on-write semantics to perform modifications to the collection. Binary search is used to locate an existing item by name, or to locate the proper insertion position if an item does not exist. A RefMap can merge up to 3 RefList collections at once during its entry iteration, allowing items in the resolved or loose RefList to override items by the same name in the packed RefList. The RefMap's goal is O(log N) lookup time, and O(N) iteration time, which is suitable for returning from a RefDatabase. By relying on the immutable RefList we might be able to make map construction nearly constant, making Repository.getAllRefs() an inexpensive operation if the caches are current. Since modification is not common, changes require up to O(N + log N) time to copy the internal list and collapse or expand the list's array. As most changes are made to the loose collection and not the packed collection, in practice most changes would require less than the full O(N) time, due to a significantly smaller N in the loose list. Almost complete test coverage is included in the corresponding unit tests. A handful of methods on RefMap are not tested in this change, as writing the proper test depends on a future refactoring of how the Ref class represents symbolic reference names. Change-Id: Ic2095274000336556f719edd75a5c5dd6dd1d857 Signed-off-by: Shawn O. Pearce --- .../org/eclipse/jgit/util/RefListTest.java | 431 +++++++++++++++++ .../tst/org/eclipse/jgit/util/RefMapTest.java | 382 +++++++++++++++ .../org/eclipse/jgit/lib/RefComparator.java | 32 +- .../src/org/eclipse/jgit/util/RefList.java | 438 ++++++++++++++++++ .../src/org/eclipse/jgit/util/RefMap.java | 423 +++++++++++++++++ 5 files changed, 1703 insertions(+), 3 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java new file mode 100644 index 000000000..c9522341b --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefListTest.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import junit.framework.TestCase; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +public class RefListTest extends TestCase { + private static final ObjectId ID = ObjectId + .fromString("41eb0d88f833b558bddeb269b7ab77399cdf98ed"); + + private static final Ref REF_A = newRef("A"); + + private static final Ref REF_B = newRef("B"); + + private static final Ref REF_c = newRef("c"); + + public void testEmpty() { + RefList list = RefList.emptyList(); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + assertFalse(list.iterator().hasNext()); + assertEquals(-1, list.find("a")); + assertEquals(-1, list.find("z")); + assertFalse(list.contains("a")); + assertNull(list.get("a")); + try { + list.get(0); + fail("RefList.emptyList should have 0 element array"); + } catch (ArrayIndexOutOfBoundsException err) { + // expected + } + } + + public void testEmptyBuilder() { + RefList list = new RefList.Builder().toRefList(); + assertEquals(0, list.size()); + assertFalse(list.iterator().hasNext()); + assertEquals(-1, list.find("a")); + assertEquals(-1, list.find("z")); + assertFalse(list.contains("a")); + assertNull(list.get("a")); + assertTrue(list.asList().isEmpty()); + assertEquals("[]", list.toString()); + + // default array capacity should be 16, with no bounds checking. + assertNull(list.get(16 - 1)); + try { + list.get(16); + fail("default RefList should have 16 element array"); + } catch (ArrayIndexOutOfBoundsException err) { + // expected + } + } + + public void testBuilder_AddThenSort() { + RefList.Builder builder = new RefList.Builder(1); + builder.add(REF_B); + builder.add(REF_A); + + RefList list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_B, list.get(0)); + assertSame(REF_A, list.get(1)); + + builder.sort(); + list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_A, list.get(0)); + assertSame(REF_B, list.get(1)); + } + + public void testBuilder_AddAll() { + RefList.Builder builder = new RefList.Builder(1); + Ref[] src = { REF_A, REF_B, REF_c, REF_A }; + builder.addAll(src, 1, 2); + + RefList list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_B, list.get(0)); + assertSame(REF_c, list.get(1)); + } + + public void testBuilder_Set() { + RefList.Builder builder = new RefList.Builder(); + builder.add(REF_A); + builder.add(REF_A); + + assertEquals(2, builder.size()); + assertSame(REF_A, builder.get(0)); + assertSame(REF_A, builder.get(1)); + + RefList list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_A, list.get(0)); + assertSame(REF_A, list.get(1)); + builder.set(1, REF_B); + + list = builder.toRefList(); + assertEquals(2, list.size()); + assertSame(REF_A, list.get(0)); + assertSame(REF_B, list.get(1)); + } + + public void testBuilder_Remove() { + RefList.Builder builder = new RefList.Builder(); + builder.add(REF_A); + builder.add(REF_B); + builder.remove(0); + + assertEquals(1, builder.size()); + assertSame(REF_B, builder.get(0)); + } + + public void testSet() { + RefList one = toList(REF_A, REF_A); + RefList two = one.set(1, REF_B); + assertNotSame(one, two); + + // one is not modified + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_A, one.get(1)); + + // but two is + assertEquals(2, two.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testAddToEmptyList() { + RefList one = toList(); + RefList two = one.add(0, REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(0, one.size()); + assertEquals(1, two.size()); + assertFalse(two.isEmpty()); + assertSame(REF_B, two.get(0)); + } + + public void testAddToFrontOfList() { + RefList one = toList(REF_A); + RefList two = one.add(0, REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(1, one.size()); + assertSame(REF_A, one.get(0)); + assertEquals(2, two.size()); + assertSame(REF_B, two.get(0)); + assertSame(REF_A, two.get(1)); + } + + public void testAddToEndOfList() { + RefList one = toList(REF_A); + RefList two = one.add(1, REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(1, one.size()); + assertSame(REF_A, one.get(0)); + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testAddToMiddleOfListByInsertionPosition() { + RefList one = toList(REF_A, REF_c); + + assertEquals(-2, one.find(REF_B.getName())); + + RefList two = one.add(one.find(REF_B.getName()), REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_c, one.get(1)); + + assertEquals(3, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + assertSame(REF_c, two.get(2)); + } + + public void testPutNewEntry() { + RefList one = toList(REF_A, REF_c); + RefList two = one.put(REF_B); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_c, one.get(1)); + + assertEquals(3, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + assertSame(REF_c, two.get(2)); + } + + public void testPutReplaceEntry() { + Ref otherc = newRef(REF_c.getName()); + assertNotSame(REF_c, otherc); + + RefList one = toList(REF_A, REF_c); + RefList two = one.put(otherc); + assertNotSame(one, two); + + // one is not modified, but two is + assertEquals(2, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_c, one.get(1)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(otherc, two.get(1)); + } + + public void testRemoveFrontOfList() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.remove(0); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_B, two.get(0)); + assertSame(REF_c, two.get(1)); + } + + public void testRemoveMiddleOfList() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.remove(1); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_c, two.get(1)); + } + + public void testRemoveEndOfList() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.remove(2); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testRemoveMakesEmpty() { + RefList one = toList(REF_A); + RefList two = one.remove(1); + assertNotSame(one, two); + assertSame(two, RefList.emptyList()); + } + + public void testToString() { + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(REF_A); + exp.append(", "); + exp.append(REF_B); + exp.append("]"); + + RefList list = toList(REF_A, REF_B); + assertEquals(exp.toString(), list.toString()); + } + + public void testBuilder_ToString() { + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(REF_A); + exp.append(", "); + exp.append(REF_B); + exp.append("]"); + + RefList.Builder list = new RefList.Builder(); + list.add(REF_A); + list.add(REF_B); + assertEquals(exp.toString(), list.toString()); + } + + public void testFindContainsGet() { + RefList list = toList(REF_A, REF_B, REF_c); + + assertEquals(0, list.find("A")); + assertEquals(1, list.find("B")); + assertEquals(2, list.find("c")); + + assertEquals(-1, list.find("0")); + assertEquals(-2, list.find("AB")); + assertEquals(-3, list.find("a")); + assertEquals(-4, list.find("z")); + + assertSame(REF_A, list.get("A")); + assertSame(REF_B, list.get("B")); + assertSame(REF_c, list.get("c")); + assertNull(list.get("AB")); + assertNull(list.get("z")); + + assertTrue(list.contains("A")); + assertTrue(list.contains("B")); + assertTrue(list.contains("c")); + assertFalse(list.contains("AB")); + assertFalse(list.contains("z")); + } + + public void testIterable() { + RefList list = toList(REF_A, REF_B, REF_c); + + int idx = 0; + for (Ref ref : list) + assertSame(list.get(idx++), ref); + assertEquals(3, idx); + + Iterator i = RefList.emptyList().iterator(); + try { + i.next(); + fail("did not throw NoSuchElementException"); + } catch (NoSuchElementException err) { + // expected + } + + i = list.iterator(); + assertTrue(i.hasNext()); + assertSame(REF_A, i.next()); + try { + i.remove(); + fail("did not throw UnsupportedOperationException"); + } catch (UnsupportedOperationException err) { + // expected + } + } + + public void testCopyLeadingPrefix() { + RefList one = toList(REF_A, REF_B, REF_c); + RefList two = one.copy(2).toRefList(); + assertNotSame(one, two); + + assertEquals(3, one.size()); + assertSame(REF_A, one.get(0)); + assertSame(REF_B, one.get(1)); + assertSame(REF_c, one.get(2)); + + assertEquals(2, two.size()); + assertSame(REF_A, two.get(0)); + assertSame(REF_B, two.get(1)); + } + + public void testCopyConstructorReusesArray() { + RefList.Builder one = new RefList.Builder(); + one.add(REF_A); + + RefList two = new RefList(one.toRefList()); + one.set(0, REF_B); + assertSame(REF_B, two.get(0)); + } + + private RefList toList(Ref... refs) { + RefList.Builder b = new RefList.Builder(refs.length); + b.addAll(refs, 0, refs.length); + return b.toRefList(); + } + + private static Ref newRef(final String name) { + return new Ref(Ref.Storage.LOOSE, name, ID); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java new file mode 100644 index 000000000..6ce206e01 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +import junit.framework.TestCase; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +public class RefMapTest extends TestCase { + private static final ObjectId ID_ONE = ObjectId + .fromString("41eb0d88f833b558bddeb269b7ab77399cdf98ed"); + + private static final ObjectId ID_TWO = ObjectId + .fromString("698dd0b8d0c299f080559a1cffc7fe029479a408"); + + private RefList packed; + + private RefList loose; + + private RefList resolved; + + protected void setUp() throws Exception { + super.setUp(); + packed = RefList.emptyList(); + loose = RefList.emptyList(); + resolved = RefList.emptyList(); + } + + public void testEmpty_NoPrefix1() { + RefMap map = new RefMap("", packed, loose, resolved); + assertTrue(map.isEmpty()); // before size was computed + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); // after size was computed + + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.keySet().iterator().hasNext()); + assertFalse(map.containsKey("a")); + assertNull(map.get("a")); + } + + public void testEmpty_NoPrefix2() { + RefMap map = new RefMap(); + assertTrue(map.isEmpty()); // before size was computed + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); // after size was computed + + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.keySet().iterator().hasNext()); + assertFalse(map.containsKey("a")); + assertNull(map.get("a")); + } + + public void testNotEmpty_NoPrefix() { + final Ref master = newRef("refs/heads/master", ID_ONE); + packed = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + assertFalse(map.isEmpty()); // before size was computed + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); // after size was computed + assertSame(master, map.values().iterator().next()); + } + + public void testEmpty_WithPrefix() { + final Ref master = newRef("refs/heads/master", ID_ONE); + packed = toList(master); + + RefMap map = new RefMap("refs/tags/", packed, loose, resolved); + assertTrue(map.isEmpty()); // before size was computed + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); // after size was computed + + assertFalse(map.entrySet().iterator().hasNext()); + assertFalse(map.keySet().iterator().hasNext()); + } + + public void testNotEmpty_WithPrefix() { + final Ref master = newRef("refs/heads/master", ID_ONE); + packed = toList(master); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + assertFalse(map.isEmpty()); // before size was computed + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); // after size was computed + assertSame(master, map.values().iterator().next()); + } + + public void testClear() { + final Ref master = newRef("refs/heads/master", ID_ONE); + loose = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + assertSame(master, map.get("refs/heads/master")); + + map.clear(); + assertNull(map.get("refs/heads/master")); + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + } + + public void testIterator_RefusesRemove() { + final Ref master = newRef("refs/heads/master", ID_ONE); + loose = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(master, itr.next()); + try { + itr.remove(); + fail("iterator allowed remove"); + } catch (UnsupportedOperationException err) { + // expected + } + } + + public void testIterator_FailsAtEnd() { + final Ref master = newRef("refs/heads/master", ID_ONE); + loose = toList(master); + + RefMap map = new RefMap("", packed, loose, resolved); + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(master, itr.next()); + try { + itr.next(); + fail("iterator allowed next"); + } catch (NoSuchElementException err) { + // expected + } + } + + public void testMerge_PackedLooseLoose() { + final Ref refA = newRef("A", ID_ONE); + final Ref refB_ONE = newRef("B", ID_ONE); + final Ref refB_TWO = newRef("B", ID_TWO); + final Ref refc = newRef("c", ID_ONE); + + packed = toList(refA, refB_ONE); + loose = toList(refB_TWO, refc); + + RefMap map = new RefMap("", packed, loose, resolved); + assertEquals(3, map.size()); + assertFalse(map.isEmpty()); + assertTrue(map.containsKey(refA.getName())); + assertSame(refA, map.get(refA.getName())); + + // loose overrides packed given same name + assertSame(refB_TWO, map.get(refB_ONE.getName())); + + Iterator itr = map.values().iterator(); + assertTrue(itr.hasNext()); + assertSame(refA, itr.next()); + assertTrue(itr.hasNext()); + assertSame(refB_TWO, itr.next()); + assertTrue(itr.hasNext()); + assertSame(refc, itr.next()); + assertFalse(itr.hasNext()); + } + + public void testMerge_WithPrefix() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/foo/bar/B", ID_TWO); + final Ref c = newRef("refs/heads/foo/rab/C", ID_TWO); + final Ref g = newRef("refs/heads/g", ID_ONE); + packed = toList(a, b, c, g); + + RefMap map = new RefMap("refs/heads/foo/", packed, loose, resolved); + assertEquals(2, map.size()); + + assertSame(b, map.get("bar/B")); + assertSame(c, map.get("rab/C")); + assertNull(map.get("refs/heads/foo/bar/B")); + assertNull(map.get("refs/heads/A")); + + assertTrue(map.containsKey("bar/B")); + assertTrue(map.containsKey("rab/C")); + assertFalse(map.containsKey("refs/heads/foo/bar/B")); + assertFalse(map.containsKey("refs/heads/A")); + + Iterator> itr = map.entrySet().iterator(); + Map.Entry ent; + assertTrue(itr.hasNext()); + ent = itr.next(); + assertEquals("bar/B", ent.getKey()); + assertSame(b, ent.getValue()); + assertTrue(itr.hasNext()); + ent = itr.next(); + assertEquals("rab/C", ent.getKey()); + assertSame(c, ent.getValue()); + assertFalse(itr.hasNext()); + } + + public void testPut_KeyMustMatchName_NoPrefix() { + final Ref refA = newRef("refs/heads/A", ID_ONE); + RefMap map = new RefMap("", packed, loose, resolved); + try { + map.put("FOO", refA); + fail("map accepted invalid key/value pair"); + } catch (IllegalArgumentException err) { + // expected + } + } + + public void testPut_KeyMustMatchName_WithPrefix() { + final Ref refA = newRef("refs/heads/A", ID_ONE); + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + try { + map.put("FOO", refA); + fail("map accepted invalid key/value pair"); + } catch (IllegalArgumentException err) { + // expected + } + } + + public void testPut_NoPrefix() { + final Ref refA_one = newRef("refs/heads/A", ID_ONE); + final Ref refA_two = newRef("refs/heads/A", ID_TWO); + + packed = toList(refA_one); + + RefMap map = new RefMap("", packed, loose, resolved); + assertSame(refA_one, map.get(refA_one.getName())); + assertSame(refA_one, map.put(refA_one.getName(), refA_two)); + + // map changed, but packed, loose did not + assertSame(refA_two, map.get(refA_one.getName())); + assertSame(refA_one, packed.get(0)); + assertEquals(0, loose.size()); + + assertSame(refA_two, map.put(refA_one.getName(), refA_one)); + assertSame(refA_one, map.get(refA_one.getName())); + } + + public void testPut_WithPrefix() { + final Ref refA_one = newRef("refs/heads/A", ID_ONE); + final Ref refA_two = newRef("refs/heads/A", ID_TWO); + + packed = toList(refA_one); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + assertSame(refA_one, map.get("A")); + assertSame(refA_one, map.put("A", refA_two)); + + // map changed, but packed, loose did not + assertSame(refA_two, map.get("A")); + assertSame(refA_one, packed.get(0)); + assertEquals(0, loose.size()); + + assertSame(refA_two, map.put("A", refA_one)); + assertSame(refA_one, map.get("A")); + } + + public void testToString_NoPrefix() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/B", ID_TWO); + + packed = toList(a, b); + + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(a.toString()); + exp.append(", "); + exp.append(b.toString()); + exp.append("]"); + + RefMap map = new RefMap("", packed, loose, resolved); + assertEquals(exp.toString(), map.toString()); + } + + public void testToString_WithPrefix() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/foo/B", ID_TWO); + final Ref c = newRef("refs/heads/foo/C", ID_TWO); + final Ref g = newRef("refs/heads/g", ID_ONE); + + packed = toList(a, b, c, g); + + StringBuilder exp = new StringBuilder(); + exp.append("["); + exp.append(b.toString()); + exp.append(", "); + exp.append(c.toString()); + exp.append("]"); + + RefMap map = new RefMap("refs/heads/foo/", packed, loose, resolved); + assertEquals(exp.toString(), map.toString()); + } + + public void testEntryType() { + final Ref a = newRef("refs/heads/A", ID_ONE); + final Ref b = newRef("refs/heads/B", ID_TWO); + + packed = toList(a, b); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + Iterator> itr = map.entrySet().iterator(); + Map.Entry ent_a = itr.next(); + Map.Entry ent_b = itr.next(); + + assertEquals(ent_a.hashCode(), "A".hashCode()); + assertTrue(ent_a.equals(ent_a)); + assertFalse(ent_a.equals(ent_b)); + + assertEquals(a.toString(), ent_a.toString()); + } + + public void testEntryTypeSet() { + final Ref refA_one = newRef("refs/heads/A", ID_ONE); + final Ref refA_two = newRef("refs/heads/A", ID_TWO); + + packed = toList(refA_one); + + RefMap map = new RefMap("refs/heads/", packed, loose, resolved); + assertSame(refA_one, map.get("A")); + + Map.Entry ent = map.entrySet().iterator().next(); + assertEquals("A", ent.getKey()); + assertSame(refA_one, ent.getValue()); + + assertSame(refA_one, ent.setValue(refA_two)); + assertSame(refA_two, ent.getValue()); + assertSame(refA_two, map.get("A")); + assertEquals(1, map.size()); + } + + private RefList toList(Ref... refs) { + RefList.Builder b = new RefList.Builder(refs.length); + b.addAll(refs, 0, refs.length); + return b.toRefList(); + } + + private static Ref newRef(String name, ObjectId id) { + return new Ref(Ref.Storage.LOOSE, name, id); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java index fe6d210c1..f14488b73 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2008, Charles O'Farrell - * Copyright (C) 2008, Google Inc. + * Copyright (C) 2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -61,12 +61,12 @@ public class RefComparator implements Comparator { public static final RefComparator INSTANCE = new RefComparator(); public int compare(final Ref o1, final Ref o2) { - return o1.getOrigName().compareTo(o2.getOrigName()); + return compareTo(o1, o2); } /** * Sorts the collection of refs, returning a new collection. - * + * * @param refs * collection to be sorted * @return sorted collection of refs @@ -76,4 +76,30 @@ public static Collection sort(final Collection refs) { Collections.sort(r, INSTANCE); return r; } + + /** + * Compare a reference to a name. + * + * @param o1 + * the reference instance. + * @param o2 + * the name to compare to. + * @return standard Comparator result of < 0, 0, > 0. + */ + public static int compareTo(Ref o1, String o2) { + return o1.getName().compareTo(o2); + } + + /** + * Compare two references by name. + * + * @param o1 + * the reference instance. + * @param o2 + * the other reference instance. + * @return standard Comparator result of < 0, 0, > 0. + */ + public static int compareTo(final Ref o1, final Ref o2) { + return o1.getName().compareTo(o2.getName()); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java new file mode 100644 index 000000000..45b065999 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; + +/** + * Specialized variant of an ArrayList to support a {@code RefDatabase}. + *

+ * This list is a hybrid of a Map<String,Ref> and of a List<Ref>. It + * tracks reference instances by name by keeping them sorted and performing + * binary search to locate an entry. Lookup time is O(log N), but addition and + * removal is O(N + log N) due to the list expansion or contraction costs. + *

+ * This list type is copy-on-write. Mutation methods return a new copy of the + * list, leaving {@code this} unmodified. As a result we cannot easily implement + * the {@link java.util.List} interface contract. + * + * @param + * the type of reference being stored in the collection. + */ +public class RefList implements Iterable { + private static final RefList EMPTY = new RefList(new Ref[0], 0); + + /** + * @return an empty unmodifiable reference list. + * @param + * the type of reference being stored in the collection. + */ + @SuppressWarnings("unchecked") + public static RefList emptyList() { + return (RefList) EMPTY; + } + + private final Ref[] list; + + private final int cnt; + + RefList(Ref[] list, int cnt) { + this.list = list; + this.cnt = cnt; + } + + /** + * Initialize this list to use the same backing array as another list. + * + * @param src + * the source list. + */ + protected RefList(RefList src) { + this.list = src.list; + this.cnt = src.cnt; + } + + public Iterator iterator() { + return new Iterator() { + private int idx; + + public boolean hasNext() { + return idx < cnt; + } + + public Ref next() { + if (idx < cnt) + return list[idx++]; + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** @return this cast as an immutable, standard {@link java.util.List}. */ + public final List asList() { + final List r = Arrays.asList(list).subList(0, cnt); + return Collections.unmodifiableList(r); + } + + /** @return number of items in this list. */ + public final int size() { + return cnt; + } + + /** @return true if the size of this list is 0. */ + public final boolean isEmpty() { + return cnt == 0; + } + + /** + * Locate an entry by name. + * + * @param name + * the name of the reference to find. + * @return the index the reference is at. If the entry is not present + * returns a negative value. The insertion position for the given + * name can be computed from {@code -(index + 1)}. + */ + public final int find(String name) { + int high = cnt; + if (high == 0) + return -1; + int low = 0; + do { + final int mid = (low + high) >>> 1; + final int cmp = RefComparator.compareTo(list[mid], name); + if (cmp < 0) + low = mid + 1; + else if (cmp == 0) + return mid; + else + high = mid; + } while (low < high); + return -(low + 1); + } + + /** + * Determine if a reference is present. + * + * @param name + * name of the reference to find. + * @return true if the reference is present; false if it is not. + */ + public final boolean contains(String name) { + return 0 <= find(name); + } + + /** + * Get a reference object by name. + * + * @param name + * the name of the reference. + * @return the reference object; null if it does not exist in this list. + */ + public final T get(String name) { + int idx = find(name); + return 0 <= idx ? get(idx) : null; + } + + /** + * Get the reference at a particular index. + * + * @param idx + * the index to obtain. Must be {@code 0 <= idx < size()}. + * @return the reference value, never null. + */ + @SuppressWarnings("unchecked") + public final T get(int idx) { + return (T) list[idx]; + } + + /** + * Obtain a builder initialized with the first {@code n} elements. + *

+ * Copies the first {@code n} elements from this list into a new builder, + * which can be used by the caller to add additional elements. + * + * @param n + * the number of elements to copy. + * @return a new builder with the first {@code n} elements already added. + */ + public final Builder copy(int n) { + Builder r = new Builder(Math.max(16, n)); + r.addAll(list, 0, n); + return r; + } + + /** + * Obtain a new copy of the list after changing one element. + *

+ * This list instance is not affected by the replacement. Because this + * method copies the entire list, it runs in O(N) time. + * + * @param idx + * index of the element to change. + * @param ref + * the new value, must not be null. + * @return copy of this list, after replacing {@code idx} with {@code ref} . + */ + public final RefList set(int idx, T ref) { + Ref[] newList = new Ref[cnt]; + System.arraycopy(list, 0, newList, 0, cnt); + newList[idx] = ref; + return new RefList(newList, cnt); + } + + /** + * Add an item at a specific index. + *

+ * This list instance is not affected by the addition. Because this method + * copies the entire list, it runs in O(N) time. + * + * @param idx + * position to add the item at. If negative the method assumes it + * was a direct return value from {@link #find(String)} and will + * adjust it to the correct position. + * @param ref + * the new reference to insert. + * @return copy of this list, after making space for and adding {@code ref}. + */ + public final RefList add(int idx, T ref) { + if (idx < 0) + idx = -(idx + 1); + + Ref[] newList = new Ref[cnt + 1]; + if (0 < idx) + System.arraycopy(list, 0, newList, 0, idx); + newList[idx] = ref; + if (idx < cnt) + System.arraycopy(list, idx, newList, idx + 1, cnt - idx); + return new RefList(newList, cnt + 1); + } + + /** + * Remove an item at a specific index. + *

+ * This list instance is not affected by the addition. Because this method + * copies the entire list, it runs in O(N) time. + * + * @param idx + * position to remove the item from. + * @return copy of this list, after making removing the item at {@code idx}. + */ + public final RefList remove(int idx) { + if (cnt == 1) + return emptyList(); + Ref[] newList = new Ref[cnt - 1]; + if (0 < idx) + System.arraycopy(list, 0, newList, 0, idx); + if (idx + 1 < cnt) + System.arraycopy(list, idx + 1, newList, idx, cnt - (idx + 1)); + return new RefList(newList, cnt - 1); + } + + /** + * Store a reference, adding or replacing as necessary. + *

+ * This list instance is not affected by the store. The correct position is + * determined, and the item is added if missing, or replaced if existing. + * Because this method copies the entire list, it runs in O(N + log N) time. + * + * @param ref + * the reference to store. + * @return copy of this list, after performing the addition or replacement. + */ + public final RefList put(T ref) { + int idx = find(ref.getName()); + if (0 <= idx) + return set(idx, ref); + return add(idx, ref); + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append('['); + if (cnt > 0) { + r.append(list[0]); + for (int i = 1; i < cnt; i++) { + r.append(", "); + r.append(list[i]); + } + } + r.append(']'); + return r.toString(); + } + + /** + * Builder to facilitate fast construction of an immutable RefList. + * + * @param + * type of reference being stored. + */ + public static class Builder { + private Ref[] list; + + private int size; + + /** Create an empty list ready for items to be added. */ + public Builder() { + this(16); + } + + /** + * Create an empty list with at least the specified capacity. + * + * @param capacity + * the new capacity. + */ + public Builder(int capacity) { + list = new Ref[capacity]; + } + + /** @return number of items in this builder's internal collection. */ + public int size() { + return size; + } + + /** + * Get the reference at a particular index. + * + * @param idx + * the index to obtain. Must be {@code 0 <= idx < size()}. + * @return the reference value, never null. + */ + @SuppressWarnings("unchecked") + public T get(int idx) { + return (T) list[idx]; + } + + /** + * Remove an item at a specific index. + * + * @param idx + * position to remove the item from. + */ + public void remove(int idx) { + System.arraycopy(list, idx + 1, list, idx, size - (idx + 1)); + size--; + } + + /** + * Add the reference to the end of the array. + *

+ * References must be added in sort order, or the array must be sorted + * after additions are complete using {@link #sort()}. + * + * @param ref + */ + public void add(T ref) { + if (list.length == size) { + Ref[] n = new Ref[size * 2]; + System.arraycopy(list, 0, n, 0, size); + list = n; + } + list[size++] = ref; + } + + /** + * Add all items from a source array. + *

+ * References must be added in sort order, or the array must be sorted + * after additions are complete using {@link #sort()}. + * + * @param src + * the source array. + * @param off + * position within {@code src} to start copying from. + * @param cnt + * number of items to copy from {@code src}. + */ + public void addAll(Ref[] src, int off, int cnt) { + if (list.length < size + cnt) { + Ref[] n = new Ref[Math.max(size * 2, size + cnt)]; + System.arraycopy(list, 0, n, 0, size); + list = n; + } + System.arraycopy(src, off, list, size, cnt); + size += cnt; + } + + /** + * Replace a single existing element. + * + * @param idx + * index, must have already been added previously. + * @param ref + * the new reference. + */ + public void set(int idx, T ref) { + list[idx] = ref; + } + + /** Sort the list's backing array in-place. */ + public void sort() { + Arrays.sort(list, 0, size, RefComparator.INSTANCE); + } + + /** @return an unmodifiable list using this collection's backing array. */ + public RefList toRefList() { + return new RefList(list, size); + } + + @Override + public String toString() { + return toRefList().toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java new file mode 100644 index 000000000..8a21cff73 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +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 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 RefComparator} ordering. + * The map's internal representation is a sorted array of {@link 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 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 '/'. + */ + private final String prefix; + + /** Immutable collection of the packed references at construction time. */ + private 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. + */ + private 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. + */ + private RefList resolved; + + private int size; + + private boolean sizeIsValid; + + private Set> entrySet; + + /** Construct an empty map with a small initial capacity. */ + public RefMap() { + prefix = ""; + 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}. + */ + public RefMap(String prefix, RefList packed, RefList loose, + RefList resolved) { + this.prefix = prefix; + this.packed = packed; + this.loose = loose; + this.resolved = resolved; + } + + @Override + public boolean containsKey(Object name) { + return get(name) != null; + } + + @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; + } + + @Override + public Ref put(final 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; + } else { + Ref prior = get(keyName); + loose = loose.add(idx, value); + sizeIsValid = false; + return prior; + } + } + + @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; + } + + @Override + public boolean isEmpty() { + return entrySet().isEmpty(); + } + + @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; + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + boolean first = true; + r.append('['); + for (Ref ref : values()) { + if (first) + first = false; + else + r.append(", "); + r.append(ref); + } + r.append(']'); + return r.toString(); + } + + private String toRefName(String name) { + if (0 < prefix.length()) + name = prefix + name; + return name; + } + + private 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); + } + } + + public boolean hasNext() { + if (next == null) + next = peek(); + return next != null; + } + + 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(final 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; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private class Ent implements Entry { + private Ref ref; + + Ent(Ref ref) { + this.ref = ref; + } + + public String getKey() { + return toMapKey(ref); + } + + public Ref getValue() { + return ref; + } + + 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.equals(a, b)) + return true; + } + } + } + return false; + } + + @Override + public String toString() { + return ref.toString(); + } + } +}