]> source.dussan.org Git - jackcess.git/commitdiff
rework current row page tracking with RowState
authorJames Ahlborn <jtahlborn@yahoo.com>
Thu, 14 Sep 2006 19:24:12 +0000 (19:24 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Thu, 14 Sep 2006 19:24:12 +0000 (19:24 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@109 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/PageChannel.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/TempPageHolder.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/UsageMap.java
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java

index fe336f3d131e34e51a7af593e14aaf01163f3034..27f204b692e63f8f9f041661bbf903426c8ff3f2 100644 (file)
@@ -43,6 +43,8 @@ public class PageChannel implements Channel {
   
   private static final Log LOG = LogFactory.getLog(PageChannel.class);
   
+  static final int INVALID_PAGE_NUMBER = -1;
+  
   /** Global usage map always lives on page 1 */
   private static final int PAGE_GLOBAL_USAGE_MAP = 1;
   
@@ -131,5 +133,19 @@ public class PageChannel implements Channel {
   public boolean isOpen() {
     return _channel.isOpen();
   }
+
+  /**
+   * @return a duplicate of the current buffer narrowed to the given position
+   *         and limit.
+   */
+  public static ByteBuffer narrowBuffer(ByteBuffer buffer, int position,
+                                        int limit)
+  {
+    return (ByteBuffer)buffer.duplicate()
+      .order(buffer.order())
+      .clear()
+      .limit(limit)
+      .position(position);
+  }
   
 }
index 9e213f3638e4ba0f79b7123c90f26adecdba7a22..8f7367009b60c3773da8e7508795e0176b51b2d0 100644 (file)
@@ -53,23 +53,25 @@ public class Table
   
   private static final Log LOG = LogFactory.getLog(Table.class);
 
+  private static final int INVALID_ROW_NUMBER = -1;
+  
   private static final short OFFSET_MASK = (short)0x1FFF;
 
   private static final short DELETED_ROW_MASK = (short)0x8000;
   
   private static final short OVERFLOW_ROW_MASK = (short)0x4000;
-  
+
   /** Table type code for system tables */
   public static final byte TYPE_SYSTEM = 0x53;
   /** Table type code for user tables */
   public static final byte TYPE_USER = 0x4e;
   
-  /** Buffer used for reading the table */
-  private ByteBuffer _rowBuffer;
+  /** State used for reading the table rows */
+  private RowState _rowState;
   /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */
   private byte _tableType;
   /** Number of the current row in a data page */
-  private int _currentRowInPage;
+  private int _currentRowInPage = INVALID_ROW_NUMBER;
   /** Number of indexes on the table */
   private int _indexCount;
   /** Number of index slots for the table */
@@ -104,12 +106,6 @@ public class Table
   private UsageMap _ownedPages;
   /** Usage map of pages that this table owns with free space on them */
   private UsageMap _freeSpacePages;
-  /** buffer used for reading overflow pages */
-  private ByteBuffer _overflowRowBuffer;
-  /** the page number of the currently read overflow page */
-  private int _overflowPageNumber;
-  /** true if the current row is an overflow row */
-  private boolean _overflowRow;
   
   /**
    * Only used by unit tests
@@ -150,10 +146,10 @@ public class Table
       tableBuffer = newBuffer;
       tableBuffer.flip();
     }
-    readPage(tableBuffer);
+    readTableDefinition(tableBuffer);
     tableBuffer = null;
 
-    _rowBuffer = _pageChannel.createPageBuffer();
+    _rowState = new RowState(true);
   }
 
   /**
@@ -169,6 +165,7 @@ public class Table
   public List<Column> getColumns() {
     return Collections.unmodifiableList(_columns);
   }
+  
   /**
    * Only called by unit tests
    */
@@ -192,13 +189,14 @@ public class Table
   }
   
   /**
-   * After calling this method, getNextRow will return the first row in the table
+   * After calling this method, getNextRow will return the first row in the
+   * table
    */
   public void reset() {
     _rowsLeftOnPage = 0;
+    _currentRowInPage = INVALID_ROW_NUMBER;
     _ownedPages.reset();
-    _currentRowInPage = 0;
-    _overflowRow = false;
+    _rowState.reset();
   }
   
   /**
@@ -212,13 +210,17 @@ public class Table
    * Delete the current row (retrieved by a call to {@link #getNextRow}).
    */
   public void deleteCurrentRow() throws IOException {
-    if (_currentRowInPage == 0) {
+    if (_currentRowInPage == INVALID_ROW_NUMBER) {
       throw new IllegalStateException("Must call getNextRow first");
     }
-    int index = getRowStartOffset(_currentRowInPage - 1, _format);
-    _rowBuffer.putShort(index, (short) (_rowBuffer.getShort(index)
-                                     | DELETED_ROW_MASK | OVERFLOW_ROW_MASK));
-    _pageChannel.writePage(_rowBuffer, _ownedPages.getCurrentPageNumber());
+
+    // delete flag always gets set in the "root" page (even if overflow row)
+    ByteBuffer rowBuffer = _rowState.getPage(_pageChannel);
+    int index = getRowStartOffset(_currentRowInPage, _format);
+    rowBuffer.putShort(index, (short)(rowBuffer.getShort(index)
+                                      | DELETED_ROW_MASK | OVERFLOW_ROW_MASK));
+    writeDataPage(rowBuffer, _rowState.getPageNumber());
+    _rowState.setDeleted(true);
   }
   
   /**
@@ -228,22 +230,23 @@ public class Table
   public Map<String, Object> getNextRow(Collection<String> columnNames) 
     throws IOException
   {
-    if (!positionAtNextRow()) {
+    // find next row
+    ByteBuffer rowBuffer = positionAtNextRow();
+    if (rowBuffer == null) {
       return null;
     }
-
-    // figure out which buffer to use
-    ByteBuffer rowBuffer = ((!_overflowRow) ? _rowBuffer : _overflowRowBuffer);
+    
+    // keep track of row bounds
+    int rowStart = rowBuffer.position();
+    int rowEnd = rowBuffer.limit();
     
     if (LOG.isDebugEnabled()) {
-      LOG.debug("Data block at position " + Integer.toHexString(rowBuffer.position()) +
-          ":\n" + ByteUtil.toHexString(rowBuffer, rowBuffer.position(),
-          rowBuffer.limit() - rowBuffer.position()));
+      LOG.debug("Data block at position " +
+                Integer.toHexString(rowBuffer.position()) +
+                ":\n" + ByteUtil.toHexString(rowBuffer, rowStart,
+                                             (rowEnd - rowStart)));
     }
     
-    // keep track of where the row starts
-    int rowStart = rowBuffer.position();
-    
     short columnCount = rowBuffer.getShort(); //Number of columns in this row
 
     // keep track of where the row data starts
@@ -251,23 +254,22 @@ public class Table
     
     // read null mask
     NullMask nullMask = new NullMask(columnCount);
-    rowBuffer.position(rowBuffer.limit() - nullMask.byteSize());  //Null mask at end
+    rowBuffer.position(rowEnd - nullMask.byteSize());  //Null mask at end
     nullMask.read(rowBuffer);
 
-    short rowVarColumnCount = 0;
     short[] varColumnOffsets = null;
-    short lastVarColumnStart = 0;
     // if _maxVarColumnCount is 0, then row info does not include varcol info
     if(_maxVarColumnCount > 0) {
-      rowBuffer.position(rowBuffer.limit() - nullMask.byteSize() - 2);
-      rowVarColumnCount = rowBuffer.getShort();  // number of variable length columns in this row
-
-      //Read in the offsets of each of the variable length columns
-      varColumnOffsets = new short[rowVarColumnCount];
-      rowBuffer.position(rowBuffer.position() - 2 - (rowVarColumnCount * 2) - 2);
-      lastVarColumnStart = rowBuffer.getShort();
-      for (short i = 0; i < rowVarColumnCount; i++) {
-        varColumnOffsets[i] = rowBuffer.getShort();
+      rowBuffer.position(rowEnd - nullMask.byteSize() - 2);
+      short rowVarColumnCount = rowBuffer.getShort();  // number of variable length columns in this row
+
+      // Read in the offsets of each of the variable length columns (in
+      // reverse order)
+      varColumnOffsets = new short[rowVarColumnCount + 1];
+      int varColumnOffsetPos = rowBuffer.position() - 4;
+      for (short i = 0; i < (rowVarColumnCount + 1); i++) {
+        varColumnOffsets[i] = rowBuffer.getShort(varColumnOffsetPos);
+        varColumnOffsetPos -= 2;
       }
     }
 
@@ -297,12 +299,9 @@ public class Table
             else
             {
               // find var length column data
-              int varDataIdx = (rowVarColumnCount -
-                                column.getVarLenTableIndex() - 1);
-              int varDataStart = varColumnOffsets[varDataIdx];
-              int varDataEnd = ((varDataIdx > 0) ?
-                                varColumnOffsets[varDataIdx - 1] :
-                                lastVarColumnStart);
+              int varDataIdx = column.getVarLenTableIndex();
+              short varDataStart = varColumnOffsets[varDataIdx];
+              short varDataEnd = varColumnOffsets[varDataIdx + 1];
               colDataPos = rowStart + varDataStart;
               colDataLen = varDataEnd - varDataStart;
             }
@@ -324,138 +323,141 @@ public class Table
   
   /**
    * Position the buffer at the next row in the table
-   * @return True if another row was found, false if there are no more rows
+   * @return a ByteBuffer narrowed to the next row, or null if none
    */
-  private boolean positionAtNextRow() throws IOException {
+  private ByteBuffer positionAtNextRow() throws IOException {
 
     // loop until we find the next valid row or run out of pages
     while(true) {
+
+      // prepare to read new row
+      _rowState.reset();
       
       if (_rowsLeftOnPage == 0) {
-        do {
-          if (!_ownedPages.getNextPage(_rowBuffer)) {
-            //No more owned pages.  No more rows.
-            return false;
-          }
-        } while (_rowBuffer.get() != PageTypes.DATA);  //Only interested in data pages
-        _rowsLeftOnPage = _rowBuffer.getShort(_format.OFFSET_NUM_ROWS_ON_DATA_PAGE);
-        _currentRowInPage = 0;
+
+        // reset row number
+        _currentRowInPage = INVALID_ROW_NUMBER;
+        
+        Integer nextPageNumber = _ownedPages.getNextPage();
+        if (nextPageNumber == null) {
+          //No more owned pages.  No more rows.
+          return null;
+        }
+
+        // load new page
+        ByteBuffer rowBuffer = _rowState.setPage(_pageChannel, nextPageNumber);
+        if(rowBuffer.get() != PageTypes.DATA) {
+          //Only interested in data pages
+          continue;
+        }
+
+        _rowsLeftOnPage = rowBuffer.getShort(_format.OFFSET_NUM_ROWS_ON_DATA_PAGE);
+        if(_rowsLeftOnPage == 0) {
+          // no rows on this page?
+          continue;
+        }
+        
       }
 
-      int curRow = _currentRowInPage++;
+      // move to next row
+      _currentRowInPage++;
       _rowsLeftOnPage--;
-      _overflowRow = false;
 
-      if(positionAtRow(_rowBuffer, curRow)) {
-        return true;
+      // reset row state to current "root" page
+      _rowState.setPage(_pageChannel);
+
+      ByteBuffer rowBuffer =
+        positionAtRow(_rowState, _currentRowInPage, _pageChannel, _format);
+      if(rowBuffer != null) {
+        return rowBuffer;
       }
     }
   }
 
   /**
-   * Sets the position and limit in the given row buffer according to the
-   * given row number and row end.
+   * Sets the position and limit in a new buffer using the given rowState
+   * according to the given row number and row end, following overflow row
+   * pointers as necessary.
    * 
-   * @return <code>true</code> if set to valid row, <code>false</code> if the
-   *         given row was deleted.  sets up the _overflow* fields
-   *         apppropriately as a side effect.
+   * @return a ByteBuffer narrowed to the next row, or null if row was deleted
    */
-  private boolean positionAtRow(ByteBuffer rowBuffer, int rowNum)
+  private static ByteBuffer positionAtRow(RowState rowState, int rowNum,
+                                          PageChannel pageChannel,
+                                          JetFormat format)
     throws IOException
   {
-    // note, we don't use findRowStart here cause we need the unmasked value
-    short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, _format));
-    short rowEnd = findRowEnd(rowBuffer, rowNum, _format);
-
-    boolean deletedRow = ((rowStart & DELETED_ROW_MASK) != 0);
-    boolean overflowRow = ((rowStart & OVERFLOW_ROW_MASK) != 0);
 
-    if(deletedRow ^ overflowRow) {
-      if(LOG.isDebugEnabled()) {
-        LOG.debug("Row flags: deletedRow " + deletedRow + ", overflowRow " +
-                  overflowRow);
+    while(true) {
+      ByteBuffer rowBuffer = rowState.getFinalPage();
+    
+      // note, we don't use findRowStart here cause we need the unmasked value
+      short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, format));
+      short rowEnd = findRowEnd(rowBuffer, rowNum, format);
+
+      // note, if we are reading from an overflow page, the row will be marked
+      // as deleted on that page, so ignore the deletedRow flag on overflow
+      // pages
+      boolean deletedRow =
+        (((rowStart & DELETED_ROW_MASK) != 0) && !rowState.isOverflow());
+      boolean overflowRow = ((rowStart & OVERFLOW_ROW_MASK) != 0);
+
+      if(deletedRow ^ overflowRow) {
+        if(LOG.isDebugEnabled()) {
+          LOG.debug("Row flags: deletedRow " + deletedRow + ", overflowRow " +
+                    overflowRow);
+        }
       }
-    }
 
-    rowStart = (short)(rowStart & OFFSET_MASK);
+      // now, strip flags from rowStart offset
+      rowStart = (short)(rowStart & OFFSET_MASK);
 
-    // note, if we are reading from an overflow page, the row will be marked
-    // as deleted on that page, so ignore the deletedRow flag on overflow
-    // pages
-    if (deletedRow && !_overflowRow) {
+      if (deletedRow) {
       
-      // Deleted row.  Skip.
-      if(LOG.isDebugEnabled()) {
-        LOG.debug("Skipping deleted row");
-      }
-      return false;
+        // Deleted row.  Skip.
+        if(LOG.isDebugEnabled()) {
+          LOG.debug("Skipping deleted row");
+        }
+        rowState.setDeleted(true);
+        return null;
       
-    } else if (overflowRow) {
+      } else if (overflowRow) {
 
-      if((rowEnd - rowStart) < 4) {
-        throw new IOException("invalid overflow row info");
-      }
-      
-      // Overflow page.  the "row" data in the current page points to another
-      // page/row
-      rowBuffer.position(rowStart);
-      rowBuffer.limit(rowEnd);
-      
-      int overflowRowNum = rowBuffer.get();
-      int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer);
-      return positionAtOverflowPage(overflowPageNum, overflowRowNum);
+        if((rowEnd - rowStart) < 4) {
+          throw new IOException("invalid overflow row info");
+        }
       
-    } else {
+        // Overflow page.  the "row" data in the current page points to another
+        // page/row
+        int overflowRowNum = rowBuffer.get(rowStart);
+        int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1);
+        rowState.setOverflowPage(pageChannel, overflowPageNum);
+
+        // reset row number and move to overflow page
+        rowNum = overflowRowNum;
       
-      rowBuffer.position(rowStart);
-      rowBuffer.limit(rowEnd);
-      return true;
-    }
-    
+      } else {
+
+        return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd);
+      }
+    }    
   }
 
   /**
-   * Sets the _overflow* fields appropriately for the given overflow page/row
-   * info.
-   * 
-   * @return <code>true</code> if set to valid row, <code>false</code> if the
-   *         given row was deleted.
+   * Reads a single column from the given row in the given buffer.
    */
-  private boolean positionAtOverflowPage(int pageNumber, int rowNum)
-    throws IOException
-  {
-    if((_overflowRowBuffer == null) || (_overflowPageNumber != pageNumber)) {
-
-      // need to load page
-      if(_overflowRowBuffer == null) {
-        // create buffer
-        _overflowRowBuffer = _pageChannel.createPageBuffer();
-      }
-
-      // read page
-      _overflowPageNumber = pageNumber;
-      _pageChannel.readPage(_overflowRowBuffer, _overflowPageNumber);
-
-    }
-
-    // indicate that the row data is in the overflow page
-    _overflowRow = true;
-    
-    // find position on overflow page
-    return positionAtRow(_overflowRowBuffer, rowNum);
-  }
-  
-  public static Object readRowColumn(ByteBuffer buffer, int rowNum,
-                                     Column column, JetFormat format)
+  public static Object readRowSingleColumn(ByteBuffer buffer, int rowNum,
+                                           Column column, JetFormat format)
     throws IOException
   {
-    int rowStart = findRowStart(buffer, rowNum, format);
-    int rowEnd = findRowEnd(buffer, rowNum, format);
+    // FIXME, doesn't support overflow row
+    short rowStart = findRowStart(buffer, rowNum, format);
+    short rowEnd = findRowEnd(buffer, rowNum, format);
 
+    buffer.position(rowStart);
     short columnCount = buffer.getShort(); //Number of columns in this row
     
     NullMask nullMask = new NullMask(columnCount);
-    buffer.position(buffer.limit() - nullMask.byteSize());  //Null mask at end
+    buffer.position(rowEnd - nullMask.byteSize());  //Null mask at end
     nullMask.read(buffer);
 
     boolean isNull = nullMask.isNull(column.getColumnNumber());
@@ -479,20 +481,14 @@ public class Table
     } else {
 
       // read var length value
-      short rowVarColumnCount = 0;
-      short lastVarColumnStart = 0;
-
-      // FIXME
-//       buffer.position(buffer.limit() - nullMask.byteSize() - 2);
-//       rowVarColumnCount = buffer.getShort();  // number of variable length columns in this row
-
-//       //Read in the offsets of each of the variable length columns
-//       varColumnOffsets = new short[rowVarColumnCount];
-//       buffer.position(buffer.position() - 2 - (rowVarColumnCount * 2) - 2);
-//       lastVarColumnStart = buffer.getShort();
-//       for (short i = 0; i < rowVarColumnCount; i++) {
-//         varColumnOffsets[i] = buffer.getShort();
-//       }
+      int varColumnOffsetPos =
+        (rowEnd - nullMask.byteSize() - 4) -
+        (column.getVarLenTableIndex() * 2);
+
+      short varDataStart = buffer.getShort(varColumnOffsetPos);
+      short varDataEnd = buffer.getShort(varColumnOffsetPos - 2);
+      colDataPos = rowStart + varDataStart;
+      colDataLen = varDataEnd - varDataStart;
     }
 
     // parse the column data
@@ -531,7 +527,8 @@ public class Table
   /**
    * Read the table definition
    */
-  private void readPage(ByteBuffer tableBuffer) throws IOException {
+  private void readTableDefinition(ByteBuffer tableBuffer) throws IOException
+  {
     if (LOG.isDebugEnabled()) {
       tableBuffer.rewind();
       LOG.debug("Table def block:\n" + ByteUtil.toHexString(tableBuffer,
@@ -636,6 +633,20 @@ public class Table
     // reset to end of index info
     tableBuffer.position(idxEndOffset);
   }
+
+  /**
+   * Writes the given page data to the given page number, clears any other
+   * relevant buffers.
+   */
+  private void writeDataPage(ByteBuffer pageBuffer, int pageNumber)
+    throws IOException
+  {
+    // write the page data
+    _pageChannel.writePage(pageBuffer, pageNumber);
+
+    // if the overflow buffer is this page, invalidate it
+    _rowState.possiblyInvalidate(pageNumber, pageBuffer);
+  }
   
   /**
    * Add a single row to this table and write it to disk
@@ -682,7 +693,7 @@ public class Table
       if (freeSpaceInPage < rowSpaceUsage) {
 
         //Last data page is full.  Create a new one.
-        _pageChannel.writePage(dataPage, pageNumber);
+        writeDataPage(dataPage, pageNumber);
         dataPage.clear();
         _freeSpacePages.removePageNumber(pageNumber);
 
@@ -706,7 +717,7 @@ public class Table
         index.addRow((Object[]) rows.get(i), pageNumber, (byte) rowCount);
       }
     }
-    _pageChannel.writePage(dataPage, pageNumber);
+    writeDataPage(dataPage, pageNumber);
     
     //Update tdef page
     ByteBuffer tdefPage = _pageChannel.createPageBuffer();
@@ -972,11 +983,97 @@ public class Table
         _next = getNextRow(_columnNames);
         return rtn;
       } catch(IOException e) {
-        e.printStackTrace(System.out);
         throw new IllegalStateException(e);
       }
     }
     
   }
 
+  /**
+   * Maintains the state of reading a row of data.
+   */
+  public static class RowState
+  {
+    /** Buffer used for reading the row data pages */
+    private TempPageHolder _rowBufferH;
+    /** true if the current row is an overflow row */
+    public boolean _overflow;
+    /** true if the current row is a deleted row */
+    public boolean _deleted;
+    /** buffer used for reading overflow pages */
+    public TempPageHolder _overflowRowBufferH =
+      TempPageHolder.newHolder(false);
+    /** the row buffer which contains the final data (after following any
+        overflow pointers) */
+    public ByteBuffer _finalRowBuffer;
+
+    public RowState(boolean hardRowBuffer) {
+      _rowBufferH = TempPageHolder.newHolder(hardRowBuffer);
+    }
+    
+    public void reset() {
+      _finalRowBuffer = null;
+      _deleted = false;
+      _overflow = false;
+    }
+
+    public int getPageNumber() {
+      return _rowBufferH.getPageNumber();
+    }
+
+    public ByteBuffer getFinalPage()
+      throws IOException
+    {
+      return _finalRowBuffer;
+    }
+
+    public void setDeleted(boolean deleted) {
+      _deleted = deleted;
+    }
+
+    public boolean isDeleted() {
+      return _deleted;
+    }
+    
+    public boolean isOverflow() {
+      return _overflow;
+    }
+
+    public void possiblyInvalidate(int modifiedPageNumber,
+                                   ByteBuffer modifiedBuffer) {
+      _rowBufferH.possiblyInvalidate(modifiedPageNumber,
+                                     modifiedBuffer);
+      _overflowRowBufferH.possiblyInvalidate(modifiedPageNumber,
+                                             modifiedBuffer);
+    }
+    
+    public ByteBuffer getPage(PageChannel pageChannel)
+      throws IOException
+    {
+      return _rowBufferH.getPage(pageChannel);
+    }
+
+    public ByteBuffer setPage(PageChannel pageChannel)
+      throws IOException
+    {
+      return setPage(pageChannel, _rowBufferH.getPageNumber());
+    }
+    
+    public ByteBuffer setPage(PageChannel pageChannel, int pageNumber)
+      throws IOException
+    {
+      _finalRowBuffer = _rowBufferH.setPage(pageChannel, pageNumber);
+      return _finalRowBuffer;
+    }
+
+    public ByteBuffer setOverflowPage(PageChannel pageChannel, int pageNumber)
+      throws IOException
+    {
+      _overflow = true;
+      _finalRowBuffer = _overflowRowBufferH.setPage(pageChannel, pageNumber);
+      return _finalRowBuffer;
+    }
+
+  }
+  
 }
diff --git a/src/java/com/healthmarketscience/jackcess/TempPageHolder.java b/src/java/com/healthmarketscience/jackcess/TempPageHolder.java
new file mode 100644 (file)
index 0000000..2124bac
--- /dev/null
@@ -0,0 +1,177 @@
+// Copyright (c) 2006 Health Market Science, Inc.
+
+package com.healthmarketscience.jackcess;
+
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.nio.ByteBuffer;
+
+/**
+ * Manages a reference to a page buffer.
+ *
+ * @author James Ahlborn
+ */
+public abstract class TempPageHolder {
+
+  private static final SoftReference<ByteBuffer> EMPTY_BUFFER_REF =
+    new SoftReference<ByteBuffer>(null);
+  
+  protected int _pageNumber = PageChannel.INVALID_PAGE_NUMBER;
+  
+  protected TempPageHolder() {
+  }
+
+  /**
+   * Creates a new TempPageHolder.
+   * @param hard iff true, the TempPageHolder will maintain a hard reference
+   *             to the current page buffer, otherwise will maintain a
+   *             SoftReference.
+   */
+  public static TempPageHolder newHolder(boolean hard) {
+    if(hard) {
+      return new HardTempPageHolder();
+    }
+    return new SoftTempPageHolder();
+  }
+
+  /**
+   * @return the currently set page number
+   */
+  public int getPageNumber() {
+    return _pageNumber;
+  }
+
+  /**
+   * @return the page for the current page number, reading as necessary,
+   *         position and limit are unchanged
+   */
+  public ByteBuffer getPage(PageChannel pageChannel)
+    throws IOException
+  {
+    return setPage(pageChannel, _pageNumber, false);
+  }
+  
+  /**
+   * Sets the current page number and returns that page
+   * @return the page for the new page number, reading as necessary, resets
+   *         position
+   */
+  public ByteBuffer setPage(PageChannel pageChannel, int pageNumber)
+    throws IOException
+  {
+    return setPage(pageChannel, pageNumber, true);
+  }
+
+  private ByteBuffer setPage(PageChannel pageChannel, int pageNumber,
+                             boolean rewind)
+    throws IOException
+  {
+    ByteBuffer buffer = getBuffer(pageChannel);
+    if(pageNumber != _pageNumber) {
+      _pageNumber = pageNumber;
+      pageChannel.readPage(buffer, _pageNumber);
+    } else if(rewind) {
+      buffer.rewind();
+    }
+    
+    return buffer;
+  }
+
+  /**
+   * Forces any current page data to be disregarded (any
+   * <code>getPage</code>/<code>setPage</code> call must reload page data).
+   * Does not necessarily release any memory.
+   */
+  public void invalidate() {
+    possiblyInvalidate(_pageNumber, null);
+  }
+  
+  /**
+   * Forces any current page data to be disregarded if it matches the given
+   * page number (any <code>getPage</code>/<code>setPage</code> call must
+   * reload page data) and is not the given buffer.  Does not necessarily
+   * release any memory.
+   */
+  public void possiblyInvalidate(int modifiedPageNumber,
+                                 ByteBuffer modifiedBuffer) {
+    if(modifiedBuffer == getExistingBuffer()) {
+      // no worries, our buffer was the one modified (or is null, either way
+      // we'll need to reload)
+      return;
+    }
+    if(modifiedPageNumber == _pageNumber) {
+      _pageNumber = PageChannel.INVALID_PAGE_NUMBER;
+    }
+  }
+
+  /**
+   * Forces any current page data to be disregarded (any
+   * <code>getPage</code>/<code>setPage</code> call must reload page data) and
+   * releases any referenced memory.
+   */
+  public void clear() {
+    invalidate();
+  }
+
+  protected abstract ByteBuffer getExistingBuffer();
+  
+  protected abstract ByteBuffer getBuffer(PageChannel pageChannel);
+
+  /**
+   * TempPageHolder which has a hard reference to the page buffer.
+   */
+  private static class HardTempPageHolder extends TempPageHolder
+  {
+    private ByteBuffer _buffer;
+
+    @Override
+    protected ByteBuffer getExistingBuffer() {
+      return _buffer;
+    }
+    
+    @Override
+    protected ByteBuffer getBuffer(PageChannel pageChannel) {
+      if(_buffer == null) {
+        _buffer = pageChannel.createPageBuffer();
+      }
+      return _buffer;
+    }
+
+    @Override
+    public void clear() {
+      super.clear();
+      _buffer = null;
+    }
+  }
+  
+  /**
+   * TempPageHolder which has a soft reference to the page buffer.
+   */
+  private static class SoftTempPageHolder extends TempPageHolder
+  {
+    private SoftReference<ByteBuffer> _buffer = EMPTY_BUFFER_REF;
+
+    @Override
+    protected ByteBuffer getExistingBuffer() {
+      return _buffer.get();
+    }
+    
+    @Override
+    protected ByteBuffer getBuffer(PageChannel pageChannel) {
+      ByteBuffer buffer = _buffer.get();
+      if(buffer == null) {
+        buffer = pageChannel.createPageBuffer();
+        _buffer = new SoftReference<ByteBuffer>(buffer);
+      }
+      return buffer;
+    }
+
+    @Override
+    public void clear() {
+      super.clear();
+      _buffer.clear();
+    }
+  }
+  
+  
+}
index 59b6326201c453f340ba3bc013474156cf724e4a..a0f1f59afb94a9f5118b80a9d089ffbe752396ce 100644 (file)
@@ -151,25 +151,24 @@ public abstract class UsageMap {
   protected JetFormat getFormat() {
     return _format;
   }
-  
+
   /**
-   * After calling this method, getNextPage will return the first page in the map
+   * After calling this method, getNextPage will return the first page in the
+   * map
    */
   public void reset() {
     _currentPageIndex = 0;
   }
   
   /**
-   * @param buffer Buffer to read the next page into
-   * @return Whether or not there was another page to read
+   * @return non-<code>null</code> if there was another page to read,
+   *         <code>null</code> otherwise
    */
-  public boolean getNextPage(ByteBuffer buffer) throws IOException {
+  public Integer getNextPage() {
     if (_pageNumbers.size() > _currentPageIndex) {
-      Integer pageNumber = _pageNumbers.get(_currentPageIndex++);
-      _pageChannel.readPage(buffer, pageNumber);
-      return true;
+      return _pageNumbers.get(_currentPageIndex++);
     } else {
-      return false;
+      return null;
     }
   }
   
index 78dba1bd764a5cdbc571d6bca9cb0f9fddd7579b..1a65fcf6012e196917bc7a16b8f7199c67069a41 100644 (file)
@@ -26,11 +26,11 @@ public class DatabaseTest extends TestCase {
     super(name);
   }
    
-  private Database open() throws Exception {
+  static Database open() throws Exception {
     return Database.open(new File("test/data/test.mdb"));
   }
   
-  private Database create() throws Exception {
+  static Database create() throws Exception {
     File tmp = File.createTempFile("databaseTest", ".mdb");
     tmp.deleteOnExit();
     return Database.create(tmp);
@@ -116,9 +116,10 @@ public class DatabaseTest extends TestCase {
     checkColumn(columns, 8, "I", DataType.BOOLEAN);
   }
   
-  private void checkColumn(List columns, int columnNumber, String name,
+  static void checkColumn(List columns, int columnNumber, String name,
       DataType dataType)
-  throws Exception {
+    throws Exception
+  {
     Column column = (Column) columns.get(columnNumber);
     assertEquals(name, column.getName());
     assertEquals(dataType, column.getType());
@@ -333,14 +334,6 @@ public class DatabaseTest extends TestCase {
     assertEquals(expectedPKs, foundPKs);
   }
   
-  private int countRows(Table table) throws Exception {
-    int rtn = 0;
-    for(Map<String, Object> row : table) {
-      rtn++;
-    }
-    return rtn;
-  }
-
   public void testReadWithDeletedCols() throws Exception {
     Table table = Database.open(new File("test/data/delColTest.mdb")).getTable("Table1");
 
@@ -554,16 +547,16 @@ public class DatabaseTest extends TestCase {
     
   }
   
-  private Object[] createTestRow(String col1Val) {
+  static Object[] createTestRow(String col1Val) {
     return new Object[] {col1Val, "R", "McCune", 1234, (byte) 0xad, 555.66d,
         777.88f, (short) 999, new Date()};
   }
 
-  private Object[] createTestRow() {
+  static Object[] createTestRow() {
     return createTestRow("Tim");
   }
   
-  private void createTestTable(Database db) throws Exception {
+  static void createTestTable(Database db) throws Exception {
     List<Column> columns = new ArrayList<Column>();
     Column col = new Column();
     col.setName("A");
@@ -604,6 +597,14 @@ public class DatabaseTest extends TestCase {
     db.createTable("test", columns);
   }
 
+  static int countRows(Table table) throws Exception {
+    int rtn = 0;
+    for(Map<String, Object> row : table) {
+      rtn++;
+    }
+    return rtn;
+  }
+
   static void dumpDatabase(Database mdb) throws Exception {
     System.out.println("DATABASE:");
     for(Table table : mdb) {