]> source.dussan.org Git - jackcess.git/commitdiff
rework inline usage map handling to handle out-of-range cases better and with more...
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 10 Jul 2007 16:41:25 +0000 (16:41 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 10 Jul 2007 16:41:25 +0000 (16:41 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@156 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/ByteUtil.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
src/java/com/healthmarketscience/jackcess/PageChannel.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/TempPageHolder.java
src/java/com/healthmarketscience/jackcess/UsageMap.java

index fdfa9dbc8937aa1dd978f6958277e607a37e4d98..29ddecd15f1f6819368b42432ec10c08bfd24b93 100644 (file)
@@ -181,6 +181,35 @@ public final class ByteUtil {
       buffer.order(origOrder);
     }
   }
+
+  /**
+   * Sets all bits in the given byte range to 0.
+   */
+  public static void clearRange(ByteBuffer buffer, int start,
+                                int end)
+  {
+    putRange(buffer, start, end, (byte)0x00);
+  }
+
+  /**
+   * Sets all bits in the given byte range to 1.
+   */
+  public static void fillRange(ByteBuffer buffer, int start,
+                               int end)
+  {
+    putRange(buffer, start, end, (byte)0xff);
+  }
+  
+  /**
+   * Sets all bytes in the given byte range to the given byte value.
+   */
+  public static void putRange(ByteBuffer buffer, int start,
+                              int end, byte b)
+  {
+    for(int i = start; i < end; ++i) {
+      buffer.put(i, b);
+    }
+  }
   
   /**
    * Convert a byte buffer to a hexadecimal string for display
index 501d19bea990b1e77169df441eef24a21566a1d4..b88c5f67531360807dad60a34cd751f3fd762a91 100644 (file)
@@ -118,7 +118,6 @@ public abstract class JetFormat {
   public final int SIZE_COLUMN_DEF_BLOCK;
   public final int SIZE_INDEX_ENTRY_MASK;
   
-  public final int PAGES_PER_USAGE_MAP_PAGE;
   public final int USAGE_MAP_TABLE_BYTE_LENGTH;
   
   public final Charset CHARSET;
@@ -203,7 +202,6 @@ public abstract class JetFormat {
     SIZE_COLUMN_DEF_BLOCK = defineSizeColumnDefBlock();
     SIZE_INDEX_ENTRY_MASK = defineSizeIndexEntryMask();
     
-    PAGES_PER_USAGE_MAP_PAGE = definePagesPerUsageMapPage();
     USAGE_MAP_TABLE_BYTE_LENGTH = defineUsageMapTableByteLength();
     
     CHARSET = defineCharset();
@@ -266,7 +264,6 @@ public abstract class JetFormat {
   protected abstract int defineSizeColumnDefBlock();
   protected abstract int defineSizeIndexEntryMask();
   
-  protected abstract int definePagesPerUsageMapPage();
   protected abstract int defineUsageMapTableByteLength();
     
   protected abstract Charset defineCharset();
@@ -339,7 +336,6 @@ public abstract class JetFormat {
     protected int defineSizeColumnDefBlock() { return 25; }
     protected int defineSizeIndexEntryMask() { return 453; }
     
-    protected int definePagesPerUsageMapPage() { return 4092 * 8; }
     protected int defineUsageMapTableByteLength() { return 64; }
       
     protected Charset defineCharset() { return Charset.forName("UTF-16LE"); }
index b81987b4a1cfcb910e4e7ce17d90fa3cbc488700..ca824dbd2aaefefaf824ab21ebd182114df7151f 100644 (file)
@@ -72,7 +72,10 @@ public class PageChannel implements Channel {
     _autoSync = autoSync;
     //Null check only exists for unit tests.  Channel should never normally be null.
     if (channel != null) {
-      _globalUsageMap = UsageMap.read(this, PAGE_GLOBAL_USAGE_MAP, (byte) 0, format);
+      // note the global usage map is a special map where any page outside of
+      // the current range is assumed to be "on"
+      _globalUsageMap = UsageMap.read(this, PAGE_GLOBAL_USAGE_MAP, (byte) 0,
+                                      format, true);
     }
   }
   
@@ -131,10 +134,15 @@ public class PageChannel implements Channel {
    * @param page Page to write
    * @return Page number at which the page was written
    */
-  public int writeNewPage(ByteBuffer page) throws IOException {
+  public int writeNewPage(ByteBuffer page) throws IOException
+  {
     long size = _channel.size();
     page.rewind();
-    _channel.write(page, size);
+    // push the buffer to the end of the page, so that a full page's worth of
+    // data is written regardless of the incoming buffer size (we use a tiny
+    // buffer in allocateNewPage)
+    long offset = size + (_format.PAGE_SIZE - page.remaining());
+    _channel.write(page, offset);
     int pageNumber = (int) (size / _format.PAGE_SIZE);
     _globalUsageMap.removePageNumber(pageNumber);  //force is done here
     return pageNumber;
@@ -145,14 +153,8 @@ public class PageChannel implements Channel {
    * until it is written in a call to {@link #writePage}.
    */
   public int allocateNewPage() throws IOException {
-    long size = _channel.size();
-    FORCE_BYTES.rewind();
-    long offset = size + _format.PAGE_SIZE - FORCE_BYTES.remaining();
     // this will force the file to be extended with mostly undefined bytes
-    _channel.write(FORCE_BYTES, offset);
-    int pageNumber = (int) (size / _format.PAGE_SIZE);
-    _globalUsageMap.removePageNumber(pageNumber);  //force is done here
-    return pageNumber;
+    return writeNewPage(FORCE_BYTES);
   }
   
   /**
index 05c32575f1f2129bb094556cb108f503b5c1bf87..bc04545345f48a3c8b0d7c06317731c10762c618 100644 (file)
@@ -572,11 +572,13 @@ public class Table
     
     byte rowNum = tableBuffer.get(_format.OFFSET_OWNED_PAGES);
     int pageNum = ByteUtil.get3ByteInt(tableBuffer, _format.OFFSET_OWNED_PAGES + 1);
-    _ownedPages = UsageMap.read(_pageChannel, pageNum, rowNum, _format);
+    _ownedPages = UsageMap.read(_pageChannel, pageNum, rowNum, _format,
+                                false);
     _ownedPagesIterator = _ownedPages.iterator();
     rowNum = tableBuffer.get(_format.OFFSET_FREE_SPACE_PAGES);
     pageNum = ByteUtil.get3ByteInt(tableBuffer, _format.OFFSET_FREE_SPACE_PAGES + 1);
-    _freeSpacePages = UsageMap.read(_pageChannel, pageNum, rowNum, _format);
+    _freeSpacePages = UsageMap.read(_pageChannel, pageNum, rowNum, _format,
+                                    false);
     
     for (int i = 0; i < _indexCount; i++) {
       Index index = new Index(_tableDefPageNumber, _pageChannel, _format);
index d2a59a6dbbc5de63aac92ba36e61b308c7236b27..23b1b2d4787acddcd81da58ddbcf7aaf7a9aa17d 100644 (file)
@@ -78,31 +78,20 @@ public abstract class TempPageHolder {
   }
 
   /**
-   * Sets the current page number to INVALID_PAGE_NUMBER and returns a new
-   * empty buffer.  Once the new page has been written to the page channel,
-   * {@link #finishNewPage} must be called with the new page number.
+   * Allocates a new buffer in the database (with undefined data) and returns
+   * a new empty buffer.
    */
-  public ByteBuffer startNewPage(PageChannel pageChannel)
+  public ByteBuffer setNewPage(PageChannel pageChannel)
     throws IOException
   {
+    // ditch any current data
     clear();
+    // allocate a new page in the database
+    _pageNumber = pageChannel.allocateNewPage();
+    // return a new buffer
     return getBuffer(pageChannel);
   }
 
-  /**
-   * Sets the new page number for the current buffer of data.  Must be called
-   * after the buffer returned from {@link #startNewPage} has been written to
-   * the PageChannel as a new page and assigned a number.
-   */
-  public void finishNewPage(int pageNumber)
-    throws IOException
-  {
-    if(_pageNumber != PageChannel.INVALID_PAGE_NUMBER) {
-      throw new IOException("page number should not be set");
-    }
-    _pageNumber = pageNumber;
-  }
-  
   /**
    * Forces any current page data to be disregarded (any
    * <code>getPage</code>/<code>setPage</code> call must reload page data).
index 7034db6a0b535f2810c22e55b6b55b3baacbcee7..55c201576d2982568d439a5cbfc1a0fade4b629d 100644 (file)
@@ -109,7 +109,8 @@ public class UsageMap
    *         which type of map is found
    */
   public static UsageMap read(PageChannel pageChannel, int pageNum,
-                              byte rowNum, JetFormat format)
+                              byte rowNum, JetFormat format,
+                              boolean assumeOutOfRangeBitsOn)
     throws IOException
   {
     ByteBuffer tableBuffer = pageChannel.createPageBuffer();
@@ -120,15 +121,15 @@ public class UsageMap
     byte mapType = tableBuffer.get(rowStart);
     UsageMap rtn = new UsageMap(pageChannel, tableBuffer, pageNum, format,
                                 rowStart);
-    rtn.initHandler(mapType);
+    rtn.initHandler(mapType, assumeOutOfRangeBitsOn);
     return rtn;
   }
 
-  private void initHandler(byte mapType)
+  private void initHandler(byte mapType, boolean assumeOutOfRangeBitsOn)
     throws IOException
   {
     if (mapType == MAP_TYPE_INLINE) {
-      _handler = new InlineHandler();
+      _handler = new InlineHandler(assumeOutOfRangeBitsOn);
     } else if (mapType == MAP_TYPE_REFERENCE) {
       _handler = new ReferenceHandler();
     } else {
@@ -176,6 +177,10 @@ public class UsageMap
     return _startPage;
   }
     
+  protected int getEndPage() {
+    return _endPage;
+  }
+    
   protected BitSet getPageNumbers() {
     return _pageNumbers;
   }
@@ -230,6 +235,20 @@ public class UsageMap
            (pageNumber - _startPage) : -1);
   }
 
+  protected void clearTableAndPages()
+  {
+    // reset some values
+    _pageNumbers.clear();
+    _startPage = 0;
+    _endPage = 0;
+    ++_modCount;
+    
+    // clear out the table data
+    int tableStart = getRowStart() + getFormat().OFFSET_MAP_START - 4;
+    int tableEnd = tableStart + getFormat().USAGE_MAP_TABLE_BYTE_LENGTH + 4;
+    ByteUtil.clearRange(_tableBuffer, tableStart, tableEnd);
+  }
+  
   protected void writeTable()
     throws IOException
   {
@@ -240,7 +259,7 @@ public class UsageMap
   /**
    * Read in the page numbers in this inline map
    */
-  protected void processMap(ByteBuffer buffer, int pageIndex)
+  protected void processMap(ByteBuffer buffer, int bufferStartPage)
   {
     int byteCount = 0;
     while (buffer.hasRemaining()) {
@@ -248,8 +267,7 @@ public class UsageMap
       if(b != (byte)0) {
         for (int i = 0; i < 8; i++) {
           if ((b & (1 << i)) != 0) {
-            int pageNumberOffset = (byteCount * 8 + i) +
-              (pageIndex * _format.PAGES_PER_USAGE_MAP_PAGE);
+            int pageNumberOffset = (byteCount * 8 + i) + bufferStartPage;
             _pageNumbers.set(pageNumberOffset);
           }
         }
@@ -262,15 +280,6 @@ public class UsageMap
    * Add a page number to this usage map
    */
   public void addPageNumber(int pageNumber) throws IOException {
-    //Sanity check, only on in debug mode for performance considerations
-    if (LOG.isDebugEnabled()) {
-      int pageNumberOffset = pageNumber - _startPage;
-      if((pageNumberOffset < 0) ||
-         _pageNumbers.get(pageNumberOffset)) {
-        throw new IOException("Page number " + pageNumber +
-                              " already in usage map");
-      }
-    }
     ++_modCount;
     _handler.addOrRemovePageNumber(pageNumber, true);
   }
@@ -286,13 +295,23 @@ public class UsageMap
   protected void updateMap(int absolutePageNumber,
                            int bufferRelativePageNumber,
                            ByteBuffer buffer, boolean add)
+    throws IOException
   {
-    //Find the byte to apply the bitmask to
+    //Find the byte to which to apply the bitmask and create the bitmask
     int offset = bufferRelativePageNumber / 8;
     int bitmask = 1 << (bufferRelativePageNumber % 8);
     byte b = buffer.get(_startOffset + offset);
+
+    // check current value for this page number
+    int pageNumberOffset = pageNumberToBitIndex(absolutePageNumber);
+    boolean isOn = _pageNumbers.get(pageNumberOffset);
+    if(isOn == add) {
+      throw new IOException("Page number " + absolutePageNumber + " already " +
+                            ((add) ? "added to" : "removed from") +
+                            " usage map");
+    }
+    
     //Apply the bitmask
-    int pageNumberOffset = absolutePageNumber - _startPage;
     if (add) {
       b |= bitmask;
       _pageNumbers.set(pageNumberOffset);
@@ -302,27 +321,22 @@ public class UsageMap
     }
     buffer.put(_startOffset + offset, b);
   }
-  
+
+  /**
+   * Promotes and inline usage map to a reference usage map.
+   */
   private void promoteInlineHandlerToReferenceHandler(int newPageNumber)
     throws IOException
   {
-    System.out.println("FOO promoting!");
-    
     // copy current page number info to new references and then clear old
     int oldStartPage = _startPage;
     BitSet oldPageNumbers = (BitSet)_pageNumbers.clone();
-    _pageNumbers.clear();
-    _startPage = 0;
 
     // clear out the main table (inline usage map data and start page)
-    int tableStart = _startOffset - 4;
-    int tableEnd = tableStart + getFormat().USAGE_MAP_TABLE_BYTE_LENGTH + 4;
-    for(int i = tableStart; i < tableEnd; ++i) {
-      _tableBuffer.put(i, (byte)0);
-    }
-
+    clearTableAndPages();
+    
     // set the new map type
-    _tableBuffer.put(tableStart - 1, MAP_TYPE_REFERENCE);
+    _tableBuffer.put(getRowStart(), MAP_TYPE_REFERENCE);
 
     // write the new table data
     writeTable();
@@ -330,14 +344,24 @@ public class UsageMap
     // set new handler
     _handler = new ReferenceHandler();
 
-    // now add all the old pages back in
+    // update new handler with old data
+    reAddPages(oldStartPage, oldPageNumbers, newPageNumber);
+  }
+
+  private void reAddPages(int oldStartPage, BitSet oldPageNumbers,
+                          int newPageNumber)
+    throws IOException
+  {
+    // add all the old pages back in
     for(int i = oldPageNumbers.nextSetBit(0); i >= 0;
         i = oldPageNumbers.nextSetBit(i + 1)) {
       addPageNumber(oldStartPage + i);
     }
 
-    // and then add the new page
-    addPageNumber(newPageNumber);
+    if(newPageNumber != PageChannel.INVALID_PAGE_NUMBER) {
+      // and then add the new page
+      addPageNumber(newPageNumber);
+    }
   }
   
   public String toString() {
@@ -374,54 +398,186 @@ public class UsageMap
    */
   private class InlineHandler extends Handler
   {
-    private InlineHandler()
+    private final boolean _assumeOutOfRangeBitsOn;
+    
+    private InlineHandler(boolean assumeOutOfRangeBitsOn)
       throws IOException
     {
+      _assumeOutOfRangeBitsOn = assumeOutOfRangeBitsOn;
       int startPage = getTableBuffer().getInt(getRowStart() + 1);
-      setPageRange(startPage, startPage +
-                   (getFormat().USAGE_MAP_TABLE_BYTE_LENGTH * 8));
+      setInlinePageRange(startPage);
       processMap(getTableBuffer(), 0);
     }
-  
+
+    private int getMaxInlinePages() {
+      return(getFormat().USAGE_MAP_TABLE_BYTE_LENGTH * 8);
+    }
+    
+    /**
+     * Sets the page range for an inline usage map starting from the given
+     * page.
+     */
+    private void setInlinePageRange(int startPage) {
+      setPageRange(startPage, startPage + getMaxInlinePages());
+    }      
+    
     @Override
     public void addOrRemovePageNumber(int pageNumber, boolean add)
       throws IOException
     {
-      int startPage = getStartPage();
-      if (add && pageNumber < startPage) {
-        throw new IOException("Can't add page number " + pageNumber +
-                              " because it is less than start page " + startPage);
-      }
-      int relativePageNumber = pageNumber - startPage;
-      ByteBuffer buffer = getTableBuffer();
-      if ((!add && !getPageNumbers().get(relativePageNumber)) ||
-          (add && (relativePageNumber >
-                   (getFormat().USAGE_MAP_TABLE_BYTE_LENGTH * 8 - 1))))
-      {
+      if(isPageWithinRange(pageNumber)) {
+
+        // easy enough, just update the inline data
+        int bufferRelativePageNumber = pageNumberToBitIndex(pageNumber);
+        updateMap(pageNumber, bufferRelativePageNumber, getTableBuffer(), add);
+        // Write the updated map back to disk
+        writeTable();
+        
+      } else {
+
+        // uh-oh, we've split our britches.  what now?  determine what our
+        // status is
+        int firstPage = getFirstPageNumber();
+        int lastPage = getLastPageNumber();
+        
         if(add) {
-          // we need to expand to a reference handler
-          promoteInlineHandlerToReferenceHandler(pageNumber);
-          return;
-        }
-        //Increase the start page to the current page and clear out the map.
-        startPage = pageNumber;
-        setPageRange(startPage, startPage +
-                     (getFormat().USAGE_MAP_TABLE_BYTE_LENGTH * 8));
-        buffer.position(getRowStart() + 1);
-        buffer.putInt(startPage);
-        getPageNumbers().clear();
-        if (!add) {
-          for (int j = 0; j < getFormat().USAGE_MAP_TABLE_BYTE_LENGTH; j++) {
-            buffer.put((byte) 0xff); //Fill bitmap with 1s
+
+          // we can ignore out-of-range page addition if we are already
+          // assuming out-of-range bits are "on"
+          if(!_assumeOutOfRangeBitsOn) {
+            
+            // we are adding, can we shift the bits and stay inline?
+            if(firstPage == PageChannel.INVALID_PAGE_NUMBER) {
+              // no pages currently
+              firstPage = pageNumber;
+              lastPage = pageNumber;
+            } else if(pageNumber > lastPage) {
+              lastPage = pageNumber;
+            } else {
+              firstPage = pageNumber;
+            }
+            if((lastPage - firstPage + 1) < getMaxInlinePages()) {
+
+              // we can still fit within an inline map
+              moveToNewStartPage(firstPage, pageNumber);
+            
+            } else {
+              // not going to happen, need to promote the usage map to a
+              // reference map
+              promoteInlineHandlerToReferenceHandler(pageNumber);
+            }
+          }
+        } else {
+
+          // we are removing, what does that mean?
+          if(_assumeOutOfRangeBitsOn) {
+
+            // we are using an inline map and assuming that anything not
+            // within the current range is "on"
+            if((firstPage == PageChannel.INVALID_PAGE_NUMBER) ||
+               (pageNumber > lastPage)) {
+
+              // move to new start page, filling in as we move
+              moveToNewStartPageForRemove(firstPage, lastPage, pageNumber);
+              
+            } else {
+              
+              // umm, we are removing an already passed bit.  whatever, we are
+              // not going back for it
+            }
+            
+          } else {
+
+            // this should not happen, we are removing a page which is not in
+            // the map
+            throw new IOException("Page number " + pageNumber +
+                                  " already removed from usage map");
           }
-          getPageNumbers().set(0, (getFormat().USAGE_MAP_TABLE_BYTE_LENGTH * 8)); //Fill our list with page numbers
         }
-        writeTable();
-        relativePageNumber = pageNumber - startPage;
+
       }
-      updateMap(pageNumber, relativePageNumber, buffer, add);
-      //Write the updated map back to disk
+    }
+
+    /**
+     * Shifts the inline usage map so that it now starts with the given page.
+     * @param newStartPage new page at which to start
+     * @param newPageNumber optional page number to add once the map has been
+     *                      shifted to the new start page
+     */
+    private void moveToNewStartPage(int newStartPage, int newPageNumber)
+      throws IOException
+    {
+      int oldStartPage = getStartPage();
+      BitSet oldPageNumbers = (BitSet)getPageNumbers().clone();
+
+      // clear out the main table (inline usage map data and start page)
+      clearTableAndPages();
+
+      // write new start page
+      ByteBuffer tableBuffer = getTableBuffer();
+      tableBuffer.position(getRowStart() + 1);
+      tableBuffer.putInt(newStartPage);
+
+      // write the new table data
       writeTable();
+
+      // set new page range
+      setInlinePageRange(newStartPage);
+
+      // put the pages back in
+      reAddPages(oldStartPage, oldPageNumbers, newPageNumber);
+    }
+
+    /**
+     * Shifts the inline usage map so that it now starts with the given
+     * firstPage (if valid), otherwise the newPageNumber.  Any page numbers
+     * added to the end of the usage map are set to "on".
+     * @param firstPage current first used page
+     * @param lastPage current last used page
+     * @param newPageNumber page number to remove once the map has been
+     *                      shifted to the new start page
+     */
+    private void moveToNewStartPageForRemove(int firstPage,
+                                             int lastPage,
+                                             int newPageNumber)
+      throws IOException
+    {
+      int newStartPage = firstPage;
+      if(newStartPage == PageChannel.INVALID_PAGE_NUMBER) {
+        newStartPage = newPageNumber;
+      } else if((newPageNumber - newStartPage + 1) >=
+                getMaxInlinePages()) {
+        // this will not move us far enough to hold the new page.  just
+        // discard any initial unused pages
+        newStartPage += (newPageNumber - getMaxInlinePages() + 1);
+      }
+      
+      // move the current data
+      moveToNewStartPage(newStartPage, PageChannel.INVALID_PAGE_NUMBER);
+
+      if(firstPage == PageChannel.INVALID_PAGE_NUMBER) {
+        
+        // this is the common case where we left everything behind
+        int tableStart = getRowStart() + getFormat().OFFSET_MAP_START;
+        int tableEnd = tableStart + getFormat().USAGE_MAP_TABLE_BYTE_LENGTH;
+        ByteUtil.fillRange(_tableBuffer, tableStart, tableEnd);
+
+        // write out the updated table
+        writeTable();
+
+        // "add" all the page numbers
+        getPageNumbers().set(0, getMaxInlinePages());
+
+      } else {
+
+        // add every new page manually
+        for(int i = (lastPage + 1); i < getEndPage(); ++i) {
+          addPageNumber(i);
+        }
+      }
+
+      // lastly, remove the new page
+      removePageNumber(newPageNumber);
     }
   }
 
@@ -441,18 +597,16 @@ public class UsageMap
     private ReferenceHandler()
       throws IOException
     {
-      int numPages = (getFormat().USAGE_MAP_TABLE_BYTE_LENGTH / 4) + 1;
-      int numBitsPerPage = ((getFormat().PAGE_SIZE -
-                             getFormat().OFFSET_USAGE_MAP_PAGE_DATA) * 8);
+      int numUsagePages = (getFormat().USAGE_MAP_TABLE_BYTE_LENGTH / 4) + 1;
       setStartOffset(getFormat().OFFSET_USAGE_MAP_PAGE_DATA);
-      setPageRange(0, (numPages * numBitsPerPage));
+      setPageRange(0, (numUsagePages * getMaxPagesPerUsagePage()));
+      
       // there is no "start page" for a reference usage map, so we get an
       // extra page reference on top of the number of page references that fit
       // in the table
-      for (int i = 0; i < numPages; i++) {
+      for (int i = 0; i < numUsagePages; i++) {
         int mapPageNum = getTableBuffer().getInt(
-            getRowStart() + getFormat().OFFSET_REFERENCE_MAP_PAGE_NUMBERS +
-            (4 * i));
+            calculateMapPagePointerOffset(i));
         if (mapPageNum > 0) {
           ByteBuffer mapPageBuffer =
             _mapPageHolder.setPage(getPageChannel(), mapPageNum);
@@ -463,24 +617,35 @@ public class UsageMap
                                   pageType);
           }
           mapPageBuffer.position(getFormat().OFFSET_USAGE_MAP_PAGE_DATA);
-          processMap(mapPageBuffer, i);
+          processMap(mapPageBuffer, (getMaxPagesPerUsagePage() * i));
         }
       }
     }
-  
+
+    private int getMaxPagesPerUsagePage() {
+      return((getFormat().PAGE_SIZE - getFormat().OFFSET_USAGE_MAP_PAGE_DATA)
+             * 8);
+    }
+        
     @Override
     public void addOrRemovePageNumber(int pageNumber, boolean add)
       throws IOException
     {
-      int pageIndex = (int) Math.floor(pageNumber / getFormat().PAGES_PER_USAGE_MAP_PAGE);
-      int mapPageNum = getTableBuffer().getInt(calculateMapPagePointerOffset(pageIndex));
+      if(!isPageWithinRange(pageNumber)) {
+        throw new IOException("Page number " + pageNumber +
+                              " is out of supported range");
+      }
+      int pageIndex = (int)(pageNumber / getMaxPagesPerUsagePage());
+      int mapPageNum = getTableBuffer().getInt(
+          calculateMapPagePointerOffset(pageIndex));
       if(mapPageNum <= 0) {
-        //Need to create a new usage map page
+        // Need to create a new usage map page
         mapPageNum  = createNewUsageMapPage(pageIndex);
       }
       ByteBuffer mapPageBuffer = _mapPageHolder.setPage(getPageChannel(),
                                                         mapPageNum);
-      updateMap(pageNumber, pageNumber - (getFormat().PAGES_PER_USAGE_MAP_PAGE * pageIndex),
+      updateMap(pageNumber,
+                (pageNumber - (getMaxPagesPerUsagePage() * pageIndex)),
                 mapPageBuffer, add);
       getPageChannel().writePage(mapPageBuffer, mapPageNum);
     }
@@ -491,15 +656,14 @@ public class UsageMap
      * @param pageIndex Index of the page reference within the map declaration 
      */
     private int createNewUsageMapPage(int pageIndex) throws IOException {
-      ByteBuffer mapPageBuffer = _mapPageHolder.startNewPage(getPageChannel());
+      ByteBuffer mapPageBuffer = _mapPageHolder.setNewPage(getPageChannel());
       mapPageBuffer.put(PageTypes.USAGE_MAP);
       mapPageBuffer.put((byte) 0x01);  //Unknown
       mapPageBuffer.putShort((short) 0); //Unknown
       for(int i = 0; i < mapPageBuffer.limit(); ++i) {
         byte b = mapPageBuffer.get(i);
       }
-      int mapPageNum = getPageChannel().writeNewPage(mapPageBuffer);
-      _mapPageHolder.finishNewPage(mapPageNum);
+      int mapPageNum = _mapPageHolder.getPageNumber();
       getTableBuffer().putInt(calculateMapPagePointerOffset(pageIndex),
                              mapPageNum);
       writeTable();