ReftableReader.java

  1. /*
  2.  * Copyright (C) 2017, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.internal.storage.reftable;

  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static org.eclipse.jgit.internal.storage.reftable.BlockReader.decodeBlockLen;
  13. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_BLOCK_TYPE;
  14. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_FOOTER_LEN;
  15. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.FILE_HEADER_LEN;
  16. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.INDEX_BLOCK_TYPE;
  17. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.LOG_BLOCK_TYPE;
  18. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.REF_BLOCK_TYPE;
  19. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.VERSION_1;
  20. import static org.eclipse.jgit.internal.storage.reftable.ReftableConstants.isFileHeaderMagic;
  21. import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;

  22. import java.io.IOException;
  23. import java.nio.ByteBuffer;
  24. import java.text.MessageFormat;
  25. import java.util.Arrays;
  26. import java.util.zip.CRC32;

  27. import org.eclipse.jgit.internal.JGitText;
  28. import org.eclipse.jgit.internal.storage.io.BlockSource;
  29. import org.eclipse.jgit.internal.storage.reftable.BlockWriter.LogEntry;
  30. import org.eclipse.jgit.lib.AnyObjectId;
  31. import org.eclipse.jgit.lib.ObjectId;
  32. import org.eclipse.jgit.lib.Ref;
  33. import org.eclipse.jgit.lib.ReflogEntry;
  34. import org.eclipse.jgit.util.LongList;
  35. import org.eclipse.jgit.util.LongMap;
  36. import org.eclipse.jgit.util.NB;

  37. /**
  38.  * Reads a reftable formatted file.
  39.  * <p>
  40.  * {@code ReftableReader} is not thread-safe. Concurrent readers need their own
  41.  * instance to read from the same file.
  42.  */
  43. public class ReftableReader extends Reftable implements AutoCloseable {
  44.     private final BlockSource src;

  45.     private int blockSize = -1;
  46.     private long minUpdateIndex;
  47.     private long maxUpdateIndex;

  48.     private long refEnd;
  49.     private long objPosition;
  50.     private long objEnd;
  51.     private long logPosition;
  52.     private long logEnd;
  53.     private int objIdLen;

  54.     private long refIndexPosition = -1;
  55.     private long objIndexPosition = -1;
  56.     private long logIndexPosition = -1;

  57.     private BlockReader refIndex;
  58.     private BlockReader objIndex;
  59.     private BlockReader logIndex;
  60.     private LongMap<BlockReader> indexCache;

  61.     /**
  62.      * Initialize a new reftable reader.
  63.      *
  64.      * @param src
  65.      *            the file content to read.
  66.      */
  67.     public ReftableReader(BlockSource src) {
  68.         this.src = src;
  69.     }

  70.     /**
  71.      * Get the block size in bytes chosen for this file by the writer.
  72.      *
  73.      * @return the block size in bytes chosen for this file by the writer. Most
  74.      *         reads from the
  75.      *         {@link org.eclipse.jgit.internal.storage.io.BlockSource} will be
  76.      *         aligned to the block size.
  77.      * @throws java.io.IOException
  78.      *             file cannot be read.
  79.      */
  80.     public int blockSize() throws IOException {
  81.         if (blockSize == -1) {
  82.             readFileHeader();
  83.         }
  84.         return blockSize;
  85.     }

  86.     @Override
  87.     public boolean hasObjectMap() throws IOException {
  88.         if (objIndexPosition == -1) {
  89.             readFileFooter();
  90.         }

  91.         // We have the map, we have no refs, or the table is small.
  92.         return (objPosition > 0 || refEnd == 24 || refIndexPosition == 0);
  93.     }

  94.     /**
  95.      * {@inheritDoc}
  96.      */
  97.     @Override
  98.     public long minUpdateIndex() throws IOException {
  99.         if (blockSize == -1) {
  100.             readFileHeader();
  101.         }
  102.         return minUpdateIndex;
  103.     }

  104.     /**
  105.      * {@inheritDoc}
  106.      */
  107.     @Override
  108.     public long maxUpdateIndex() throws IOException {
  109.         if (blockSize == -1) {
  110.             readFileHeader();
  111.         }
  112.         return maxUpdateIndex;
  113.     }

  114.     /** {@inheritDoc} */
  115.     @Override
  116.     public RefCursor allRefs() throws IOException {
  117.         if (blockSize == -1) {
  118.             readFileHeader();
  119.         }

  120.         if (refEnd == 0) {
  121.             readFileFooter();
  122.         }
  123.         src.adviseSequentialRead(0, refEnd);

  124.         RefCursorImpl i = new RefCursorImpl(refEnd, null, false);
  125.         i.block = readBlock(0, refEnd);
  126.         return i;
  127.     }

  128.     /** {@inheritDoc} */
  129.     @Override
  130.     public RefCursor seekRef(String refName) throws IOException {
  131.         initRefIndex();

  132.         byte[] key = refName.getBytes(UTF_8);
  133.         RefCursorImpl i = new RefCursorImpl(refEnd, key, false);
  134.         i.block = seek(REF_BLOCK_TYPE, key, refIndex, 0, refEnd);
  135.         return i;
  136.     }

  137.     /** {@inheritDoc} */
  138.     @Override
  139.     public RefCursor seekRefsWithPrefix(String prefix) throws IOException {
  140.         initRefIndex();

  141.         byte[] key = prefix.getBytes(UTF_8);
  142.         RefCursorImpl i = new RefCursorImpl(refEnd, key, true);
  143.         i.block = seek(REF_BLOCK_TYPE, key, refIndex, 0, refEnd);
  144.         return i;
  145.     }

  146.     /** {@inheritDoc} */
  147.     @Override
  148.     public RefCursor byObjectId(AnyObjectId id) throws IOException {
  149.         initObjIndex();
  150.         ObjCursorImpl i = new ObjCursorImpl(refEnd, id);
  151.         if (objIndex != null) {
  152.             i.initSeek();
  153.         } else {
  154.             i.initScan();
  155.         }
  156.         return i;
  157.     }

  158.     /** {@inheritDoc} */
  159.     @Override
  160.     public LogCursor allLogs() throws IOException {
  161.         initLogIndex();
  162.         if (logPosition > 0) {
  163.             src.adviseSequentialRead(logPosition, logEnd);
  164.             LogCursorImpl i = new LogCursorImpl(logEnd, null);
  165.             i.block = readBlock(logPosition, logEnd);
  166.             return i;
  167.         }
  168.         return new EmptyLogCursor();
  169.     }

  170.     /** {@inheritDoc} */
  171.     @Override
  172.     public LogCursor seekLog(String refName, long updateIndex)
  173.             throws IOException {
  174.         initLogIndex();
  175.         if (logPosition > 0) {
  176.             byte[] key = LogEntry.key(refName, updateIndex);
  177.             byte[] match = refName.getBytes(UTF_8);
  178.             LogCursorImpl i = new LogCursorImpl(logEnd, match);
  179.             i.block = seek(LOG_BLOCK_TYPE, key, logIndex, logPosition, logEnd);
  180.             return i;
  181.         }
  182.         return new EmptyLogCursor();
  183.     }

  184.     private BlockReader seek(byte blockType, byte[] key, BlockReader idx,
  185.             long startPos, long endPos) throws IOException {
  186.         if (idx != null) {
  187.             // Walk through a possibly multi-level index to a leaf block.
  188.             BlockReader block = idx;
  189.             do {
  190.                 if (block.seekKey(key) > 0) {
  191.                     return null;
  192.                 }
  193.                 long pos = block.readPositionFromIndex();
  194.                 block = readBlock(pos, endPos);
  195.             } while (block.type() == INDEX_BLOCK_TYPE);
  196.             block.seekKey(key);
  197.             return block;
  198.         }
  199.         if (blockType == LOG_BLOCK_TYPE) {
  200.             // No index. Log blocks are irregularly sized, so we can't do binary
  201.             // search between blocks. Scan over blocks instead.
  202.             BlockReader block = readBlock(startPos, endPos);

  203.             for (;;) {
  204.                 if (block == null || block.type() != LOG_BLOCK_TYPE) {
  205.                     return null;
  206.                 }

  207.                 int result = block.seekKey(key);
  208.                 if (result <= 0) {
  209.                     // == 0 : we found the key.
  210.                     // < 0 : the key is before this block. Either the ref name is there
  211.                     // but only at a newer updateIndex, or it is absent. We leave it to
  212.                     // logcursor to distinguish between both cases.
  213.                     return block;
  214.                 }

  215.                 long pos = block.endPosition();
  216.                 if (pos >= endPos) {
  217.                     return null;
  218.                 }
  219.                 block = readBlock(pos, endPos);
  220.             }
  221.         }
  222.         return binarySearch(blockType, key, startPos, endPos);
  223.     }

  224.     private BlockReader binarySearch(byte blockType, byte[] key,
  225.             long startPos, long endPos) throws IOException {
  226.         if (blockSize == 0) {
  227.             BlockReader b = readBlock(startPos, endPos);
  228.             if (blockType != b.type()) {
  229.                 return null;
  230.             }
  231.             b.seekKey(key);
  232.             return b;
  233.         }

  234.         int low = (int) (startPos / blockSize);
  235.         int end = blocksIn(startPos, endPos);
  236.         BlockReader block = null;
  237.         do {
  238.             int mid = (low + end) >>> 1;
  239.             block = readBlock(((long) mid) * blockSize, endPos);
  240.             if (blockType != block.type()) {
  241.                 return null;
  242.             }
  243.             int cmp = block.seekKey(key);
  244.             if (cmp < 0) {
  245.                 end = mid;
  246.             } else if (cmp == 0) {
  247.                 break;
  248.             } else /* if (cmp > 0) */ {
  249.                 low = mid + 1;
  250.             }
  251.         } while (low < end);
  252.         return block;
  253.     }

  254.     private void readFileHeader() throws IOException {
  255.         readHeaderOrFooter(0, FILE_HEADER_LEN);
  256.     }

  257.     private void readFileFooter() throws IOException {
  258.         int ftrLen = FILE_FOOTER_LEN;
  259.         byte[] ftr = readHeaderOrFooter(src.size() - ftrLen, ftrLen);

  260.         CRC32 crc = new CRC32();
  261.         crc.update(ftr, 0, ftrLen - 4);
  262.         if (crc.getValue() != NB.decodeUInt32(ftr, ftrLen - 4)) {
  263.             throw new IOException(JGitText.get().invalidReftableCRC);
  264.         }

  265.         refIndexPosition = NB.decodeInt64(ftr, 24);
  266.         long p = NB.decodeInt64(ftr, 32);
  267.         objPosition = p >>> 5;
  268.         objIdLen = (int) (p & 0x1f);
  269.         objIndexPosition = NB.decodeInt64(ftr, 40);
  270.         logPosition = NB.decodeInt64(ftr, 48);
  271.         logIndexPosition = NB.decodeInt64(ftr, 56);

  272.         if (refIndexPosition > 0) {
  273.             refEnd = refIndexPosition;
  274.         } else if (objPosition > 0) {
  275.             refEnd = objPosition;
  276.         } else if (logPosition > 0) {
  277.             refEnd = logPosition;
  278.         } else {
  279.             refEnd = src.size() - ftrLen;
  280.         }

  281.         if (objPosition > 0) {
  282.             if (objIndexPosition > 0) {
  283.                 objEnd = objIndexPosition;
  284.             } else if (logPosition > 0) {
  285.                 objEnd = logPosition;
  286.             } else {
  287.                 objEnd = src.size() - ftrLen;
  288.             }
  289.         }

  290.         if (logPosition > 0) {
  291.             if (logIndexPosition > 0) {
  292.                 logEnd = logIndexPosition;
  293.             } else {
  294.                 logEnd = src.size() - ftrLen;
  295.             }
  296.         }
  297.     }

  298.     private byte[] readHeaderOrFooter(long pos, int len) throws IOException {
  299.         ByteBuffer buf = src.read(pos, len);
  300.         if (buf.position() != len) {
  301.             throw new IOException(JGitText.get().shortReadOfBlock);
  302.         }

  303.         byte[] tmp = new byte[len];
  304.         buf.flip();
  305.         buf.get(tmp);
  306.         if (!isFileHeaderMagic(tmp, 0, len)) {
  307.             throw new IOException(JGitText.get().invalidReftableFile);
  308.         }

  309.         int v = NB.decodeInt32(tmp, 4);
  310.         int version = v >>> 24;
  311.         if (VERSION_1 != version) {
  312.             throw new IOException(MessageFormat.format(
  313.                     JGitText.get().unsupportedReftableVersion,
  314.                     Integer.valueOf(version)));
  315.         }
  316.         if (blockSize == -1) {
  317.             blockSize = v & 0xffffff;
  318.         }
  319.         minUpdateIndex = NB.decodeInt64(tmp, 8);
  320.         maxUpdateIndex = NB.decodeInt64(tmp, 16);
  321.         return tmp;
  322.     }

  323.     private void initRefIndex() throws IOException {
  324.         if (refIndexPosition < 0) {
  325.             readFileFooter();
  326.         }
  327.         if (refIndex == null && refIndexPosition > 0) {
  328.             refIndex = readIndex(refIndexPosition);
  329.         }
  330.     }

  331.     private void initObjIndex() throws IOException {
  332.         if (objIndexPosition < 0) {
  333.             readFileFooter();
  334.         }
  335.         if (objIndex == null && objIndexPosition > 0) {
  336.             objIndex = readIndex(objIndexPosition);
  337.         }
  338.     }

  339.     private void initLogIndex() throws IOException {
  340.         if (logIndexPosition < 0) {
  341.             readFileFooter();
  342.         }
  343.         if (logIndex == null && logIndexPosition > 0) {
  344.             logIndex = readIndex(logIndexPosition);
  345.         }
  346.     }

  347.     private BlockReader readIndex(long pos) throws IOException {
  348.         int sz = readBlockLen(pos);
  349.         BlockReader i = new BlockReader();
  350.         i.readBlock(src, pos, sz);
  351.         i.verifyIndex();
  352.         return i;
  353.     }

  354.     private int readBlockLen(long pos) throws IOException {
  355.         int sz = pos == 0 ? FILE_HEADER_LEN + 4 : 4;
  356.         ByteBuffer tmp = src.read(pos, sz);
  357.         if (tmp.position() < sz) {
  358.             throw new IOException(JGitText.get().invalidReftableFile);
  359.         }
  360.         byte[] buf;
  361.         if (tmp.hasArray() && tmp.arrayOffset() == 0) {
  362.             buf = tmp.array();
  363.         } else {
  364.             buf = new byte[sz];
  365.             tmp.flip();
  366.             tmp.get(buf);
  367.         }
  368.         if (pos == 0 && buf[FILE_HEADER_LEN] == FILE_BLOCK_TYPE) {
  369.             return FILE_HEADER_LEN;
  370.         }
  371.         int p = pos == 0 ? FILE_HEADER_LEN : 0;
  372.         return decodeBlockLen(NB.decodeInt32(buf, p));
  373.     }

  374.     private BlockReader readBlock(long pos, long end) throws IOException {
  375.         if (indexCache != null) {
  376.             BlockReader b = indexCache.get(pos);
  377.             if (b != null) {
  378.                 return b;
  379.             }
  380.         }

  381.         int sz = blockSize;
  382.         if (sz == 0) {
  383.             sz = readBlockLen(pos);
  384.         } else if (pos + sz > end) {
  385.             sz = (int) (end - pos); // last block may omit padding.
  386.         }

  387.         BlockReader b = new BlockReader();
  388.         b.readBlock(src, pos, sz);
  389.         if (b.type() == INDEX_BLOCK_TYPE && !b.truncated()) {
  390.             if (indexCache == null) {
  391.                 indexCache = new LongMap<>();
  392.             }
  393.             indexCache.put(pos, b);
  394.         }
  395.         return b;
  396.     }

  397.     private int blocksIn(long pos, long end) {
  398.         int blocks = (int) ((end - pos) / blockSize);
  399.         return end % blockSize == 0 ? blocks : (blocks + 1);
  400.     }

  401.     /**
  402.      * Get size of the reftable, in bytes.
  403.      *
  404.      * @return size of the reftable, in bytes.
  405.      * @throws java.io.IOException
  406.      *             size cannot be obtained.
  407.      */
  408.     public long size() throws IOException {
  409.         return src.size();
  410.     }

  411.     /** {@inheritDoc} */
  412.     @Override
  413.     public void close() throws IOException {
  414.         src.close();
  415.     }

  416.     private class RefCursorImpl extends RefCursor {
  417.         private final long scanEnd;
  418.         private final byte[] match;
  419.         private final boolean prefix;

  420.         private Ref ref;
  421.         BlockReader block;

  422.         RefCursorImpl(long scanEnd, byte[] match, boolean prefix) {
  423.             this.scanEnd = scanEnd;
  424.             this.match = match;
  425.             this.prefix = prefix;
  426.         }

  427.         @Override
  428.         public boolean next() throws IOException {
  429.             for (;;) {
  430.                 if (block == null || block.type() != REF_BLOCK_TYPE) {
  431.                     return false;
  432.                 } else if (!block.next()) {
  433.                     long pos = block.endPosition();
  434.                     if (pos >= scanEnd) {
  435.                         return false;
  436.                     }
  437.                     block = readBlock(pos, scanEnd);
  438.                     continue;
  439.                 }

  440.                 block.parseKey();
  441.                 if (match != null && !block.match(match, prefix)) {
  442.                     block.skipValue();
  443.                     return false;
  444.                 }

  445.                 ref = block.readRef(minUpdateIndex);
  446.                 if (!includeDeletes && wasDeleted()) {
  447.                     continue;
  448.                 }
  449.                 return true;
  450.             }
  451.         }

  452.         @Override
  453.         public void seekPastPrefix(String prefixName) throws IOException {
  454.             initRefIndex();
  455.             byte[] key = prefixName.getBytes(UTF_8);
  456.             ByteBuffer byteBuffer = ByteBuffer.allocate(key.length + 1);
  457.             byteBuffer.put(key);
  458.             // Add the representation of the last byte lexicographically. Based on how UTF_8
  459.             // representation works, this byte will be bigger lexicographically than any
  460.             // UTF_8 character when translated into bytes, since 0xFF can never be a part of
  461.             // a UTF_8 string.
  462.             byteBuffer.put((byte) 0xFF);

  463.             block = seek(REF_BLOCK_TYPE, byteBuffer.array(), refIndex, 0, refEnd);
  464.         }

  465.         @Override
  466.         public Ref getRef() {
  467.             return ref;
  468.         }

  469.         @Override
  470.         public void close() {
  471.             // Do nothing.
  472.         }
  473.     }

  474.     private class LogCursorImpl extends LogCursor {
  475.         private final long scanEnd;
  476.         private final byte[] match;

  477.         private String refName;
  478.         private long updateIndex;
  479.         private ReflogEntry entry;
  480.         BlockReader block;

  481.         /**
  482.          * Scans logs from this table until scanEnd position.
  483.          *
  484.          * @param scanEnd
  485.          *            end of the log data in the reftable.
  486.          * @param match
  487.          *            if non-null, limits the scan to precisely that refname.
  488.          */
  489.         LogCursorImpl(long scanEnd, byte[] match) {
  490.             this.scanEnd = scanEnd;
  491.             this.match = match;
  492.         }

  493.         @Override
  494.         public boolean next() throws IOException {
  495.             for (;;) {
  496.                 if (block == null || block.type() != LOG_BLOCK_TYPE) {
  497.                     return false;
  498.                 } else if (!block.next()) {
  499.                     long pos = block.endPosition();
  500.                     if (pos >= scanEnd) {
  501.                         return false;
  502.                     }
  503.                     block = readBlock(pos, scanEnd);
  504.                     continue;
  505.                 }

  506.                 block.parseKey();
  507.                 if (match != null && !block.match(match, false)) {
  508.                     block.skipValue();
  509.                     return false;
  510.                 }

  511.                 refName = block.name();
  512.                 updateIndex = block.readLogUpdateIndex();
  513.                 entry = block.readLogEntry();
  514.                 if (entry == null && !includeDeletes) {
  515.                     continue;
  516.                 }
  517.                 return true;
  518.             }
  519.         }

  520.         @Override
  521.         public String getRefName() {
  522.             return refName;
  523.         }

  524.         @Override
  525.         public long getUpdateIndex() {
  526.             return updateIndex;
  527.         }

  528.         @Override
  529.         public ReflogEntry getReflogEntry() {
  530.             return entry;
  531.         }

  532.         @Override
  533.         public void close() {
  534.             // Do nothing.
  535.         }
  536.     }

  537.     static final LongList EMPTY_LONG_LIST = new LongList(0);

  538.     private class ObjCursorImpl extends RefCursor {
  539.         private final long scanEnd;
  540.         private final ObjectId match;

  541.         private Ref ref;
  542.         private int listIdx;

  543.         private LongList blockPos;
  544.         private BlockReader block;

  545.         ObjCursorImpl(long scanEnd, AnyObjectId id) {
  546.             this.scanEnd = scanEnd;
  547.             this.match = id.copy();
  548.         }

  549.         void initSeek() throws IOException {
  550.             byte[] rawId = new byte[OBJECT_ID_LENGTH];
  551.             match.copyRawTo(rawId, 0);
  552.             byte[] key = Arrays.copyOf(rawId, objIdLen);

  553.             BlockReader b = objIndex;
  554.             do {
  555.                 if (b.seekKey(key) > 0) {
  556.                     blockPos = EMPTY_LONG_LIST;
  557.                     return;
  558.                 }
  559.                 long pos = b.readPositionFromIndex();
  560.                 b = readBlock(pos, objEnd);
  561.             } while (b.type() == INDEX_BLOCK_TYPE);
  562.             b.seekKey(key);
  563.             while (b.next()) {
  564.                 b.parseKey();
  565.                 if (b.match(key, false)) {
  566.                     blockPos = b.readBlockPositionList();
  567.                     if (blockPos == null) {
  568.                         initScan();
  569.                         return;
  570.                     }
  571.                     break;
  572.                 }
  573.                 b.skipValue();
  574.             }
  575.             if (blockPos == null) {
  576.                 blockPos = EMPTY_LONG_LIST;
  577.             }
  578.             if (blockPos.size() > 0) {
  579.                 long pos = blockPos.get(listIdx++);
  580.                 block = readBlock(pos, scanEnd);
  581.             }
  582.         }

  583.         void initScan() throws IOException {
  584.             block = readBlock(0, scanEnd);
  585.         }

  586.         @Override
  587.         public boolean next() throws IOException {
  588.             for (;;) {
  589.                 if (block == null || block.type() != REF_BLOCK_TYPE) {
  590.                     return false;
  591.                 } else if (!block.next()) {
  592.                     long pos;
  593.                     if (blockPos != null) {
  594.                         if (listIdx >= blockPos.size()) {
  595.                             return false;
  596.                         }
  597.                         pos = blockPos.get(listIdx++);
  598.                     } else {
  599.                         pos = block.endPosition();
  600.                     }
  601.                     if (pos >= scanEnd) {
  602.                         return false;
  603.                     }
  604.                     block = readBlock(pos, scanEnd);
  605.                     continue;
  606.                 }

  607.                 block.parseKey();
  608.                 ref = block.readRef(minUpdateIndex);
  609.                 ObjectId id = ref.getObjectId();
  610.                 if (id != null && match.equals(id)
  611.                         && (includeDeletes || !wasDeleted())) {
  612.                     return true;
  613.                 }
  614.             }
  615.         }

  616.         @Override
  617.         /**
  618.          * The implementation here would not be efficient complexity-wise since it
  619.          * expected that there are a small number of refs that match the same object id.
  620.          * In such case it's better to not even use this method (as the caller might
  621.          * expect it to be efficient).
  622.          */
  623.         public void seekPastPrefix(String prefixName) throws IOException {
  624.             throw new UnsupportedOperationException();
  625.         }

  626.         @Override
  627.         public Ref getRef() {
  628.             return ref;
  629.         }

  630.         @Override
  631.         public void close() {
  632.             // Do nothing.
  633.         }
  634.     }
  635. }