fix iterators may not be closed (#43)

Change-Id: I3709d96fd2114fa782d6f28f8853b32d65fbd22b
diff --git a/pom.xml b/pom.xml
index 9ad4475..7f639e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
 
     <groupId>com.baidu.hugegraph</groupId>
     <artifactId>hugegraph-common</artifactId>
-    <version>1.7.1</version>
+    <version>1.7.2</version>
 
     <name>hugegraph-common</name>
     <url>https://github.com/hugegraph/hugegraph-common</url>
@@ -218,7 +218,7 @@
                         <manifestEntries>
                             <!-- Must be on one line, otherwise the automatic
                                  upgrade script cannot replace the version number -->
-                            <Implementation-Version>1.7.1.0</Implementation-Version>
+                            <Implementation-Version>1.7.2.0</Implementation-Version>
                         </manifestEntries>
                     </archive>
                 </configuration>
diff --git a/src/main/java/com/baidu/hugegraph/iterator/BatchMapperIterator.java b/src/main/java/com/baidu/hugegraph/iterator/BatchMapperIterator.java
new file mode 100644
index 0000000..6b2a913
--- /dev/null
+++ b/src/main/java/com/baidu/hugegraph/iterator/BatchMapperIterator.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 HugeGraph Authors
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.baidu.hugegraph.iterator;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import com.baidu.hugegraph.util.E;
+import com.baidu.hugegraph.util.InsertionOrderUtil;
+import com.google.common.collect.ImmutableList;
+
+public class BatchMapperIterator<T, R> extends WrappedIterator<R> {
+
+    private final int batch;
+    private final Iterator<T> originIterator;
+    private final Function<List<T>, Iterator<R>> mapperCallback;
+
+    private Iterator<R> batchIterator;
+
+    public BatchMapperIterator(int batch, Iterator<T> origin,
+                               Function<List<T>, Iterator<R>> mapper) {
+        E.checkArgument(batch > 0, "Expect batch > 0, but got %s", batch);
+        this.batch = batch;
+        this.originIterator = origin;
+        this.mapperCallback = mapper;
+        this.batchIterator = null;
+    }
+
+    @Override
+    protected Iterator<T> originIterator() {
+        return this.originIterator;
+    }
+
+    @Override
+    protected final boolean fetch() {
+        if (this.batchIterator != null && this.fetchFromBatch()) {
+            return true;
+        }
+
+        List<T> list = this.nextBatch();
+        if (!list.isEmpty()) {
+            assert this.batchIterator == null;
+            // Do fetch
+            this.batchIterator = this.mapperCallback.apply(list);
+            if (this.batchIterator != null && this.fetchFromBatch()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected final List<T> nextBatch() {
+        if (!this.originIterator.hasNext()) {
+            return ImmutableList.of();
+        }
+        List<T> list = InsertionOrderUtil.newList();
+        for (int i = 0; i < this.batch && this.originIterator.hasNext(); i++) {
+            T next = this.originIterator.next();
+            list.add(next);
+        }
+        return list;
+    }
+
+    protected final boolean fetchFromBatch() {
+        E.checkNotNull(this.batchIterator, "mapper results");
+        while (this.batchIterator.hasNext()) {
+            R result = this.batchIterator.next();
+            if (result != null) {
+                assert this.current == none();
+                this.current = result;
+                return true;
+            }
+        }
+        this.resetBatchIterator();
+        return false;
+    }
+
+    protected final void resetBatchIterator() {
+        if (this.batchIterator == null) {
+            return;
+        }
+        close(this.batchIterator);
+        this.batchIterator = null;
+    }
+}
diff --git a/src/main/java/com/baidu/hugegraph/iterator/IterableIterator.java b/src/main/java/com/baidu/hugegraph/iterator/CIter.java
similarity index 60%
rename from src/main/java/com/baidu/hugegraph/iterator/IterableIterator.java
rename to src/main/java/com/baidu/hugegraph/iterator/CIter.java
index eb2a966..35eda45 100644
--- a/src/main/java/com/baidu/hugegraph/iterator/IterableIterator.java
+++ b/src/main/java/com/baidu/hugegraph/iterator/CIter.java
@@ -21,32 +21,5 @@
 
 import java.util.Iterator;
 
-public class IterableIterator<T> implements Iterator<T> {
-
-    private final Iterable<T> iterable;
-    private final Iterator<T> iterator;
-
-    public IterableIterator(Iterable<T> iterable) {
-        this.iterable = iterable;
-        this.iterator = iterable.iterator();
-    }
-
-    @Override
-    public boolean hasNext() {
-        return this.iterator.hasNext();
-    }
-
-    @Override
-    public T next() {
-        return this.iterator.next();
-    }
-
-    @Override
-    public void remove() {
-        this.iterator.remove();
-    }
-
-    public Iterable<T> iterable() {
-        return iterable;
-    }
+public interface CIter<R> extends Iterator<R>, AutoCloseable, Metadatable {
 }
diff --git a/src/main/java/com/baidu/hugegraph/iterator/ExtendableIterator.java b/src/main/java/com/baidu/hugegraph/iterator/ExtendableIterator.java
index 7d03611..d9985ed 100644
--- a/src/main/java/com/baidu/hugegraph/iterator/ExtendableIterator.java
+++ b/src/main/java/com/baidu/hugegraph/iterator/ExtendableIterator.java
@@ -19,10 +19,8 @@
 
 package com.baidu.hugegraph.iterator;
 
-import java.util.ArrayList;
 import java.util.Deque;
 import java.util.Iterator;
-import java.util.List;
 import java.util.concurrent.ConcurrentLinkedDeque;
 
 import com.baidu.hugegraph.util.E;
@@ -30,13 +28,11 @@
 public class ExtendableIterator<T> extends WrappedIterator<T> {
 
     private final Deque<Iterator<T>> itors;
-    private final List<Iterator<T>> removedItors;
 
     private Iterator<T> currentIterator;
 
     public ExtendableIterator() {
         this.itors = new ConcurrentLinkedDeque<>();
-        this.removedItors = new ArrayList<>();
         this.currentIterator = null;
     }
 
@@ -62,11 +58,6 @@
 
     @Override
     public void close() throws Exception {
-        for (Iterator<T> iter : this.removedItors) {
-            if (iter instanceof AutoCloseable) {
-                ((AutoCloseable) iter).close();
-            }
-        }
         for (Iterator<T> iter : this.itors) {
             if (iter instanceof AutoCloseable) {
                 ((AutoCloseable) iter).close();
@@ -75,7 +66,7 @@
     }
 
     @Override
-    protected Iterator<?> originIterator() {
+    protected Iterator<T> originIterator() {
         return this.currentIterator;
     }
 
@@ -98,7 +89,7 @@
                 // The last one
                 return false;
             }
-            this.removedItors.add(this.itors.removeFirst());
+            close(this.itors.removeFirst());
         }
 
         assert first != null && first.hasNext();
diff --git a/src/main/java/com/baidu/hugegraph/iterator/FlatMapperFilterIterator.java b/src/main/java/com/baidu/hugegraph/iterator/FlatMapperFilterIterator.java
index 6046dd2..a222319 100644
--- a/src/main/java/com/baidu/hugegraph/iterator/FlatMapperFilterIterator.java
+++ b/src/main/java/com/baidu/hugegraph/iterator/FlatMapperFilterIterator.java
@@ -36,17 +36,17 @@
     }
 
     @Override
-    protected final boolean fetchMapped() {
-        E.checkNotNull(this.results, "mapper results");
-        while (this.results.hasNext()) {
-            R result = this.results.next();
+    protected final boolean fetchFromBatch() {
+        E.checkNotNull(this.batchIterator, "mapper results");
+        while (this.batchIterator.hasNext()) {
+            R result = this.batchIterator.next();
             if (result != null && this.filterCallback.apply(result)) {
                 assert this.current == none();
                 this.current = result;
                 return true;
             }
         }
-        this.results = null;
+        this.resetBatchIterator();
         return false;
     }
 }
diff --git a/src/main/java/com/baidu/hugegraph/iterator/FlatMapperIterator.java b/src/main/java/com/baidu/hugegraph/iterator/FlatMapperIterator.java
index 9d50d79..6b4cd98 100644
--- a/src/main/java/com/baidu/hugegraph/iterator/FlatMapperIterator.java
+++ b/src/main/java/com/baidu/hugegraph/iterator/FlatMapperIterator.java
@@ -29,48 +29,63 @@
     private final Iterator<T> originIterator;
     private final Function<T, Iterator<R>> mapperCallback;
 
-    protected Iterator<R> results;
+    protected Iterator<R> batchIterator;
 
     public FlatMapperIterator(Iterator<T> origin,
                               Function<T, Iterator<R>> mapper) {
         this.originIterator = origin;
         this.mapperCallback = mapper;
-        this.results = null;
+        this.batchIterator = null;
     }
 
     @Override
-    protected Iterator<?> originIterator() {
+    public void close() throws Exception {
+        this.resetBatchIterator();
+        super.close();
+    }
+
+    @Override
+    protected Iterator<T> originIterator() {
         return this.originIterator;
     }
 
     @Override
     protected final boolean fetch() {
-        if (this.results != null && this.fetchMapped()) {
+        if (this.batchIterator != null && this.fetchFromBatch()) {
             return true;
         }
 
         while (this.originIterator.hasNext()) {
             T next = this.originIterator.next();
-            assert this.results == null;
-            this.results = this.mapperCallback.apply(next);
-            if (this.results != null && this.fetchMapped()) {
+            assert this.batchIterator == null;
+            // Do fetch
+            this.batchIterator = this.mapperCallback.apply(next);
+            if (this.batchIterator != null && this.fetchFromBatch()) {
                 return true;
             }
         }
         return false;
     }
 
-    protected boolean fetchMapped() {
-        E.checkNotNull(this.results, "mapper results");
-        while (this.results.hasNext()) {
-            R result = this.results.next();
+    protected boolean fetchFromBatch() {
+        E.checkNotNull(this.batchIterator, "mapper results");
+        while (this.batchIterator.hasNext()) {
+            R result = this.batchIterator.next();
             if (result != null) {
                 assert this.current == none();
                 this.current = result;
                 return true;
             }
         }
-        this.results = null;
+        this.resetBatchIterator();
         return false;
     }
+
+    protected final void resetBatchIterator() {
+        if (this.batchIterator == null) {
+            return;
+        }
+        close(this.batchIterator);
+        this.batchIterator = null;
+    }
 }
diff --git a/src/main/java/com/baidu/hugegraph/iterator/ListIterator.java b/src/main/java/com/baidu/hugegraph/iterator/ListIterator.java
new file mode 100644
index 0000000..f21a436
--- /dev/null
+++ b/src/main/java/com/baidu/hugegraph/iterator/ListIterator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 HugeGraph Authors
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.baidu.hugegraph.iterator;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import com.baidu.hugegraph.util.InsertionOrderUtil;
+
+public class ListIterator<T> extends WrappedIterator<T> {
+
+    private final Iterator<T> originIterator;
+    private final Iterator<T> resultsIterator;
+    private final Collection<T> results;
+
+    public ListIterator(long capacity, Iterator<T> origin) {
+        List<T> results = InsertionOrderUtil.newList();
+        while (origin.hasNext()) {
+            if (capacity >= 0L && results.size() >= capacity) {
+                throw new IllegalArgumentException(
+                          "The iterator exceeded capacity " + capacity);
+            }
+            results.add(origin.next());
+        }
+        this.originIterator = origin;
+        this.results = Collections.unmodifiableList(results);
+        this.resultsIterator = this.results.iterator();
+    }
+
+    public ListIterator(Collection<T> origin) {
+        this.originIterator = origin.iterator();
+        this.results = origin instanceof List ?
+                       Collections.unmodifiableList((List<T>) origin) :
+                       Collections.unmodifiableCollection(origin);
+        this.resultsIterator = this.results.iterator();
+    }
+
+    @Override
+    public void remove() {
+        this.resultsIterator.remove();
+    }
+
+    public Collection<T> list() {
+        return this.results;
+    }
+
+    @Override
+    protected boolean fetch() {
+        assert this.current == none();
+        if (!this.resultsIterator.hasNext()) {
+            return false;
+        }
+        this.current = this.resultsIterator.next();
+        return true;
+    }
+
+    @Override
+    protected Iterator<T> originIterator() {
+        return this.originIterator;
+    }
+}
diff --git a/src/main/java/com/baidu/hugegraph/iterator/MapperIterator.java b/src/main/java/com/baidu/hugegraph/iterator/MapperIterator.java
index f9930f5..c02d17c 100644
--- a/src/main/java/com/baidu/hugegraph/iterator/MapperIterator.java
+++ b/src/main/java/com/baidu/hugegraph/iterator/MapperIterator.java
@@ -33,7 +33,7 @@
     }
 
     @Override
-    protected Iterator<?> originIterator() {
+    protected Iterator<T> originIterator() {
         return this.originIterator;
     }
 
diff --git a/src/main/java/com/baidu/hugegraph/iterator/WrappedIterator.java b/src/main/java/com/baidu/hugegraph/iterator/WrappedIterator.java
index 30cf072..c6e9ae7 100644
--- a/src/main/java/com/baidu/hugegraph/iterator/WrappedIterator.java
+++ b/src/main/java/com/baidu/hugegraph/iterator/WrappedIterator.java
@@ -22,8 +22,7 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
-public abstract class WrappedIterator<R>
-                implements Iterator<R>, AutoCloseable, Metadatable {
+public abstract class WrappedIterator<R> implements CIter<R> {
 
     private static final Object NONE = new Object();
 
@@ -86,6 +85,16 @@
         return (R) NONE;
     }
 
+    public static void close(Iterator<?> iterator) {
+        if (iterator instanceof AutoCloseable) {
+            try {
+                ((AutoCloseable) iterator).close();
+            } catch (Exception e) {
+                throw new IllegalStateException("Failed to close iterator");
+            }
+        }
+    }
+
     protected abstract Iterator<?> originIterator();
 
     protected abstract boolean fetch();
diff --git a/src/main/java/com/baidu/hugegraph/version/CommonVersion.java b/src/main/java/com/baidu/hugegraph/version/CommonVersion.java
index 5bfcbcb..5c4ed4b 100644
--- a/src/main/java/com/baidu/hugegraph/version/CommonVersion.java
+++ b/src/main/java/com/baidu/hugegraph/version/CommonVersion.java
@@ -27,5 +27,5 @@
 
     // The second parameter of Version.of() is for all-in-one JAR
     public static final Version VERSION = Version.of(CommonVersion.class,
-                                                     "1.7.1");
+                                                     "1.7.2");
 }
diff --git a/src/test/java/com/baidu/hugegraph/unit/UnitTestSuite.java b/src/test/java/com/baidu/hugegraph/unit/UnitTestSuite.java
index 4d292d6..dc90b35 100644
--- a/src/test/java/com/baidu/hugegraph/unit/UnitTestSuite.java
+++ b/src/test/java/com/baidu/hugegraph/unit/UnitTestSuite.java
@@ -30,10 +30,12 @@
 import com.baidu.hugegraph.unit.config.OptionSpaceTest;
 import com.baidu.hugegraph.unit.date.SafeDateFormatTest;
 import com.baidu.hugegraph.unit.event.EventHubTest;
+import com.baidu.hugegraph.unit.iterator.BatchMapperIteratorTest;
 import com.baidu.hugegraph.unit.iterator.ExtendableIteratorTest;
 import com.baidu.hugegraph.unit.iterator.FilterIteratorTest;
 import com.baidu.hugegraph.unit.iterator.FlatMapperFilterIteratorTest;
 import com.baidu.hugegraph.unit.iterator.FlatMapperIteratorTest;
+import com.baidu.hugegraph.unit.iterator.ListIteratorTest;
 import com.baidu.hugegraph.unit.iterator.MapperIteratorTest;
 import com.baidu.hugegraph.unit.license.ExtraParamTest;
 import com.baidu.hugegraph.unit.license.LicenseCreateParamTest;
@@ -74,6 +76,8 @@
     MapperIteratorTest.class,
     FlatMapperIteratorTest.class,
     FlatMapperFilterIteratorTest.class,
+    ListIteratorTest.class,
+    BatchMapperIteratorTest.class,
 
     BytesTest.class,
     CollectionUtilTest.class,
diff --git a/src/test/java/com/baidu/hugegraph/unit/iterator/BatchMapperIteratorTest.java b/src/test/java/com/baidu/hugegraph/unit/iterator/BatchMapperIteratorTest.java
new file mode 100644
index 0000000..e08ff69
--- /dev/null
+++ b/src/test/java/com/baidu/hugegraph/unit/iterator/BatchMapperIteratorTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2017 HugeGraph Authors
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.baidu.hugegraph.unit.iterator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+
+import org.junit.Test;
+
+import com.baidu.hugegraph.iterator.BatchMapperIterator;
+import com.baidu.hugegraph.testutil.Assert;
+import com.baidu.hugegraph.unit.BaseUnitTest;
+import com.baidu.hugegraph.unit.iterator.ExtendableIteratorTest.CloseableItor;
+import com.google.common.collect.ImmutableList;
+
+@SuppressWarnings("resource")
+public class BatchMapperIteratorTest extends BaseUnitTest {
+
+    private static final Iterator<Integer> EMPTY = Collections.emptyIterator();
+
+    private static final List<Integer> DATA1 = ImmutableList.of(1);
+    private static final List<Integer> DATA2 = ImmutableList.of(2, 3);
+    private static final List<Integer> DATA3 = ImmutableList.of(4, 5, 6);
+
+    private static final Function<List<Integer>, Iterator<Integer>> MAPPER =
+                         batch -> batch.iterator();
+
+    @Test
+    public void testBatchMapper() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA1.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(1), ImmutableList.copyOf(results));
+
+        results = new BatchMapperIterator<>(1, DATA2.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(2, 3),
+                            ImmutableList.copyOf(results));
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6),
+                            ImmutableList.copyOf(results));
+    }
+
+    @Test
+    public void testBatch() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(2, DATA2.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(2, 3),
+                            ImmutableList.copyOf(results));
+
+        results = new BatchMapperIterator<>(2, DATA3.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6),
+                            ImmutableList.copyOf(results));
+
+        results = new BatchMapperIterator<>(3, DATA3.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6),
+                            ImmutableList.copyOf(results));
+
+        results = new BatchMapperIterator<>(4, DATA3.iterator(), MAPPER);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6),
+                            ImmutableList.copyOf(results));
+    }
+
+    @Test
+    public void testInvalidBatch() {
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            new BatchMapperIterator<>(0, DATA1.iterator(), MAPPER);
+        });
+
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            new BatchMapperIterator<>(-1, DATA1.iterator(), MAPPER);
+        });
+    }
+
+    @Test
+    public void testHasNext() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, EMPTY, MAPPER);
+        Assert.assertFalse(results.hasNext());
+
+        results = new BatchMapperIterator<>(1, DATA1.iterator(), MAPPER);
+        Assert.assertTrue(results.hasNext());
+    }
+
+    @Test
+    public void testHasNextAndNextWithMultiTimes() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), MAPPER);
+
+        for (int i = 0; i < 5; i++) {
+            Assert.assertTrue(results.hasNext());
+        }
+
+        for (int i = 0; i < 3; i++) {
+            results.next();
+        }
+
+        Assert.assertFalse(results.hasNext());
+        Assert.assertFalse(results.hasNext());
+
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testNext() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA1.iterator(), MAPPER);
+        // Call next() without hasNext()
+        Assert.assertEquals(1, results.next());
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), MAPPER);
+        Assert.assertEquals(4, results.next());
+        Assert.assertEquals(5, results.next());
+        Assert.assertEquals(6, results.next());
+
+        results = new BatchMapperIterator<>(2, DATA3.iterator(), MAPPER);
+        Assert.assertEquals(4, results.next());
+        Assert.assertEquals(5, results.next());
+        Assert.assertEquals(6, results.next());
+    }
+
+    @Test
+    public void testNextWithMultiTimes() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA2.iterator(), MAPPER);
+
+        for (int i = 0; i < 2; i++) {
+            results.next();
+        }
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testMapperWithBatch() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), batch -> {
+            Assert.assertEquals(1, batch.size());
+            return batch.iterator();
+        });
+        Assert.assertEquals(4, results.next());
+        Assert.assertEquals(5, results.next());
+        Assert.assertEquals(6, results.next());
+        Assert.assertFalse(results.hasNext());
+
+        results = new BatchMapperIterator<>(2, DATA3.iterator(), batch -> {
+            if (batch.size() == 1) {
+                Assert.assertEquals(6, batch.get(0));
+            } else {
+                Assert.assertEquals(2, batch.size());
+            }
+            return batch.iterator();
+        });
+        Assert.assertEquals(4, results.next());
+        Assert.assertEquals(5, results.next());
+        Assert.assertEquals(6, results.next());
+        Assert.assertFalse(results.hasNext());
+
+        results = new BatchMapperIterator<>(3, DATA3.iterator(), batch -> {
+            Assert.assertEquals(3, batch.size());
+            return batch.iterator();
+        });
+        Assert.assertEquals(4, results.next());
+        Assert.assertEquals(5, results.next());
+        Assert.assertEquals(6, results.next());
+        Assert.assertFalse(results.hasNext());
+
+        results = new BatchMapperIterator<>(4, DATA3.iterator(), batch -> {
+            Assert.assertEquals(3, batch.size());
+            return batch.iterator();
+        });
+        Assert.assertEquals(4, results.next());
+        Assert.assertEquals(5, results.next());
+        Assert.assertEquals(6, results.next());
+        Assert.assertFalse(results.hasNext());
+    }
+
+    @Test
+    public void testMapperThenReturn2X() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), batch -> {
+            List<Integer> list = new ArrayList<>();
+            for (int i : batch) {
+                list.add(2 * i);
+            }
+            return list.iterator();
+        });
+
+        Assert.assertEquals(8, results.next());
+        Assert.assertEquals(10, results.next());
+        Assert.assertEquals(12, results.next());
+        Assert.assertFalse(results.hasNext());
+    }
+
+    @Test
+    public void testMapperReturnNullThenHasNext() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), batch -> {
+            return null;
+        });
+        Assert.assertFalse(results.hasNext());
+        Assert.assertFalse(results.hasNext());
+    }
+
+    @Test
+    public void testMapperReturnNullThenNext() {
+        Iterator<Integer> results;
+
+        results = new BatchMapperIterator<>(1, DATA3.iterator(), batch -> {
+            return null;
+        });
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testClose() throws Exception {
+        CloseableItor<Integer> vals = new CloseableItor<>(DATA1.iterator());
+
+        Iterator<Integer> results = new BatchMapperIterator<>(1, vals, MAPPER);
+
+        Assert.assertFalse(vals.closed());
+        ((BatchMapperIterator<?, ?>) results).close();
+        Assert.assertTrue(vals.closed());
+    }
+}
diff --git a/src/test/java/com/baidu/hugegraph/unit/iterator/ExtendableIteratorTest.java b/src/test/java/com/baidu/hugegraph/unit/iterator/ExtendableIteratorTest.java
index 1a9a635..4c7ba31 100644
--- a/src/test/java/com/baidu/hugegraph/unit/iterator/ExtendableIteratorTest.java
+++ b/src/test/java/com/baidu/hugegraph/unit/iterator/ExtendableIteratorTest.java
@@ -175,7 +175,30 @@
     }
 
     @Test
-    public void testCloseAfterNext() throws Exception {
+    public void testCloseAfterNext1() throws Exception {
+        CloseableItor<Integer> c1 = new CloseableItor<>(DATA1.iterator());
+        CloseableItor<Integer> c2 = new CloseableItor<>(DATA2.iterator());
+        CloseableItor<Integer> c3 = new CloseableItor<>(DATA3.iterator());
+
+        ExtendableIterator<Integer> results = new ExtendableIterator<>();
+        results.extend(c1).extend(c2).extend(c3);
+
+        results.next();
+        results.hasNext();
+
+        Assert.assertTrue(c1.closed()); // close after iterated
+        Assert.assertFalse(c2.closed());
+        Assert.assertFalse(c3.closed());
+
+        results.close();
+
+        Assert.assertTrue(c1.closed());
+        Assert.assertTrue(c2.closed());
+        Assert.assertTrue(c3.closed());
+    }
+
+    @Test
+    public void testCloseAfterNext3() throws Exception {
         CloseableItor<Integer> c1 = new CloseableItor<>(DATA1.iterator());
         CloseableItor<Integer> c2 = new CloseableItor<>(DATA2.iterator());
         CloseableItor<Integer> c3 = new CloseableItor<>(DATA3.iterator());
@@ -191,8 +214,8 @@
             results.next();
         }
 
-        Assert.assertFalse(c1.closed());
-        Assert.assertFalse(c2.closed());
+        Assert.assertTrue(c1.closed());
+        Assert.assertTrue(c2.closed());
         Assert.assertFalse(c3.closed());
 
         results.close();
diff --git a/src/test/java/com/baidu/hugegraph/unit/iterator/ListIteratorTest.java b/src/test/java/com/baidu/hugegraph/unit/iterator/ListIteratorTest.java
new file mode 100644
index 0000000..8ddd995
--- /dev/null
+++ b/src/test/java/com/baidu/hugegraph/unit/iterator/ListIteratorTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2017 HugeGraph Authors
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.baidu.hugegraph.unit.iterator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.junit.Test;
+
+import com.baidu.hugegraph.iterator.ListIterator;
+import com.baidu.hugegraph.testutil.Assert;
+import com.baidu.hugegraph.unit.BaseUnitTest;
+import com.baidu.hugegraph.unit.iterator.ExtendableIteratorTest.CloseableItor;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+
+@SuppressWarnings("resource")
+public class ListIteratorTest extends BaseUnitTest {
+
+    private static final Iterator<Integer> EMPTY = Collections.emptyIterator();
+
+    private static final List<Integer> DATA1 = ImmutableList.of(1);
+    private static final List<Integer> DATA2 = ImmutableList.of(2, 3);
+    private static final List<Integer> DATA3 = ImmutableList.of(4, 5, 6);
+
+    @Test
+    public void testCapacity() {
+        Iterator<Integer> results;
+
+        results = new ListIterator<>(0, EMPTY);
+        Assert.assertFalse(results.hasNext());
+        Assert.assertEquals(0, Iterators.size(results));
+
+        results = new ListIterator<>(2, DATA1.iterator());
+        Assert.assertTrue(results.hasNext());
+        Assert.assertEquals(1, Iterators.size(results));
+
+        results = new ListIterator<>(2, DATA2.iterator());
+        Assert.assertTrue(results.hasNext());
+        Assert.assertEquals(2, Iterators.size(results));
+
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            new ListIterator<>(0, DATA1.iterator());
+        });
+
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            new ListIterator<>(2, DATA3.iterator());
+        });
+    }
+
+    @Test
+    public void testList() {
+        ListIterator<Integer> results;
+
+        results = new ListIterator<>(-1, EMPTY);
+        Assert.assertEquals(ImmutableList.of(), results.list());
+
+        results = new ListIterator<>(-1, DATA1.iterator());
+        Assert.assertEquals(ImmutableList.of(1), results.list());
+
+        results = new ListIterator<>(-1, DATA2.iterator());
+        Assert.assertEquals(ImmutableList.of(2, 3), results.list());
+
+        results = new ListIterator<>(-1, DATA3.iterator());
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), results.list());
+    }
+
+    @Test
+    public void testHasNext() {
+        Iterator<Integer> origin = DATA1.iterator();
+        Assert.assertTrue(origin.hasNext());
+
+        Iterator<Integer> results = new ListIterator<>(-1, origin);
+        Assert.assertTrue(results.hasNext());
+        Assert.assertTrue(results.hasNext());
+        Assert.assertFalse(origin.hasNext());
+    }
+
+    @Test
+    public void testNext() {
+        Iterator<Integer> results = new ListIterator<>(-1, DATA1.iterator());
+        Assert.assertEquals(1, (int) results.next());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testNextAfterList() {
+        Iterator<Integer> results = new ListIterator<>(-1, DATA1.iterator());
+        Assert.assertEquals(ImmutableList.of(1),
+                            ((ListIterator<?>) results).list());
+        Assert.assertEquals(1, (int) results.next());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testNextWithMultiTimes() {
+        Iterator<Integer> results = new ListIterator<>(-1, DATA2.iterator());
+        Assert.assertEquals(2, (int) results.next());
+        Assert.assertEquals(3, (int) results.next());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testHasNextAndNext() {
+        Iterator<Integer> results = new ListIterator<>(-1, DATA1.iterator());
+        Assert.assertTrue(results.hasNext());
+        Assert.assertTrue(results.hasNext());
+        Assert.assertEquals(1, (int) results.next());
+        Assert.assertFalse(results.hasNext());
+        Assert.assertFalse(results.hasNext());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results.next();
+        });
+    }
+
+    @Test
+    public void testRemove() {
+        List<Integer> list = new ArrayList<>(DATA3);
+        ListIterator<Integer> results = new ListIterator<>(-1, list.iterator());
+
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), list);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), results.list());
+
+        Assert.assertThrows(UnsupportedOperationException.class, () -> {
+            results.remove();
+        });
+        results.next();
+        Assert.assertThrows(UnsupportedOperationException.class, () -> {
+            results.remove();
+        });
+
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), list);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), results.list());
+    }
+
+    @Test
+    public void testRemoveWithoutResult() {
+        Iterator<Integer> results = new ListIterator<>(-1, EMPTY);
+        Assert.assertThrows(UnsupportedOperationException.class, () -> {
+            results.remove();
+        });
+
+        List<Integer> list0 = new ArrayList<>();
+        Iterator<Integer> results2 = new ListIterator<>(-1, list0.iterator());
+        Assert.assertThrows(UnsupportedOperationException.class, () -> {
+            results2.remove();
+        });
+    }
+
+    @Test
+    public void testClose() throws Exception {
+        CloseableItor<Integer> c1 = new CloseableItor<>(DATA1.iterator());
+
+        ListIterator<Integer> results = new ListIterator<>(-1, c1);
+
+        Assert.assertFalse(c1.closed());
+
+        results.close();
+
+        Assert.assertTrue(c1.closed());
+    }
+
+    @Test
+    public void testListWithConstructFromList() {
+        ListIterator<Integer> results;
+
+        results = new ListIterator<>(ImmutableList.of());
+        Assert.assertEquals(ImmutableList.of(), results.list());
+
+        results = new ListIterator<>(DATA1);
+        Assert.assertEquals(ImmutableList.of(1), results.list());
+
+        results = new ListIterator<>(DATA2);
+        Assert.assertEquals(ImmutableList.of(2, 3), results.list());
+
+        results = new ListIterator<>(DATA3);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), results.list());
+    }
+
+    @Test
+    public void testHasNextAndNextWithConstructFromList() {
+        ListIterator<Integer> results0 = new ListIterator<>(ImmutableList.of());
+        Assert.assertFalse(results0.hasNext());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results0.next();
+        });
+
+        ListIterator<Integer> results1 = new ListIterator<>(DATA1);
+        Assert.assertTrue(results1.hasNext());
+        Assert.assertEquals(1, results1.next());
+        Assert.assertFalse(results1.hasNext());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results1.next();
+        });
+
+        ListIterator<Integer> results3 = new ListIterator<>(DATA3);
+        Assert.assertTrue(results3.hasNext());
+        Assert.assertEquals(4, results3.next());
+        Assert.assertTrue(results3.hasNext());
+        Assert.assertEquals(5, results3.next());
+        Assert.assertTrue(results3.hasNext());
+        Assert.assertEquals(6, results3.next());
+        Assert.assertFalse(results3.hasNext());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results3.next();
+        });
+    }
+
+    @Test
+    public void testNextWithConstructFromList() {
+        ListIterator<Integer> results0 = new ListIterator<>(ImmutableList.of());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results0.next();
+        });
+
+        ListIterator<Integer> results3 = new ListIterator<>(DATA3);
+        Assert.assertEquals(4, results3.next());
+        Assert.assertEquals(5, results3.next());
+        Assert.assertEquals(6, results3.next());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results3.next();
+        });
+    }
+
+    @Test
+    public void testNextAfterListWithConstructFromList() {
+        ListIterator<Integer> results3 = new ListIterator<>(DATA3);
+        Assert.assertEquals(ImmutableList.of(4, 5, 6), results3.list());
+        Assert.assertEquals(4, results3.next());
+        Assert.assertEquals(5, results3.next());
+        Assert.assertEquals(6, results3.next());
+        Assert.assertThrows(NoSuchElementException.class, () -> {
+            results3.next();
+        });
+    }
+}