View Javadoc

1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   This library is free software; you can redistribute it and/or
5   modify it under the terms of the GNU Lesser General Public
6   License as published by the Free Software Foundation; either
7   version 2.1 of the License, or (at your option) any later version.
8   
9   This library is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  Lesser General Public License for more details.
13  
14  You should have received a copy of the GNU Lesser General Public
15  License along with this library; if not, write to the Free Software
16  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
17  USA
18  
19  You can contact Health Market Science at info@healthmarketscience.com
20  or at the following address:
21  
22  Health Market Science
23  2700 Horizon Drive
24  Suite 200
25  King of Prussia, PA 19406
26  */
27  
28  package com.healthmarketscience.jackcess;
29  
30  import java.io.Flushable;
31  import java.io.IOException;
32  import java.nio.ByteBuffer;
33  import java.nio.ByteOrder;
34  import java.nio.channels.Channel;
35  import java.nio.channels.FileChannel;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  
40  /**
41   * Reads and writes individual pages in a database file
42   * @author Tim McCune
43   */
44  public class PageChannel implements Channel, Flushable {
45    
46    private static final Log LOG = LogFactory.getLog(PageChannel.class);
47    
48    static final int INVALID_PAGE_NUMBER = -1;
49  
50    static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
51    
52    /** invalid page header, used when deallocating old pages.  data pages
53        generally have 4 interesting bytes at the beginning which we want to
54        reset. */
55    private static final byte[] INVALID_PAGE_BYTE_HEADER =
56      new byte[]{PageTypes.INVALID, (byte)0, (byte)0, (byte)0};
57    
58    /** Global usage map always lives on page 1 */
59    private static final int PAGE_GLOBAL_USAGE_MAP = 1;
60    /** Global usage map always lives at row 0 */
61    private static final int ROW_GLOBAL_USAGE_MAP = 0;
62    
63    /** Channel containing the database */
64    private final FileChannel _channel;
65    /** Format of the database in the channel */
66    private final JetFormat _format;
67    /** whether or not to force all writes to disk immediately */
68    private final  boolean _autoSync;
69    /** buffer used when deallocating old pages.  data pages generally have 4
70        interesting bytes at the beginning which we want to reset. */
71    private final ByteBuffer _invalidPageBytes =
72      ByteBuffer.wrap(INVALID_PAGE_BYTE_HEADER);
73    /** dummy buffer used when allocating new pages */
74    private final ByteBuffer _forceBytes = ByteBuffer.allocate(1);
75    /** Tracks free pages in the database. */
76    private UsageMap _globalUsageMap;
77    
78    /**
79     * @param channel Channel containing the database
80     * @param format Format of the database in the channel
81     */
82    public PageChannel(FileChannel channel, JetFormat format, boolean autoSync)
83      throws IOException
84    {
85      _channel = channel;
86      _format = format;
87      _autoSync = autoSync;
88    }
89  
90    /**
91     * Does second-stage initialization, must be called after construction.
92     */
93    public void initialize(Database database)
94      throws IOException
95    {
96      // note the global usage map is a special map where any page outside of
97      // the current range is assumed to be "on"
98      _globalUsageMap = UsageMap.read(database, PAGE_GLOBAL_USAGE_MAP,
99                                      ROW_GLOBAL_USAGE_MAP, true);
100   }
101   
102   /**
103    * Only used by unit tests
104    */
105   PageChannel(boolean testing) {
106     if(!testing) {
107       throw new IllegalArgumentException();
108     }
109     _channel = null;
110     _format = JetFormat.VERSION_4;
111     _autoSync = false;
112   }
113 
114   public JetFormat getFormat() {
115     return _format;
116   }
117 
118   /**
119    * Returns the next page number based on the given file size.
120    */
121   private int getNextPageNumber(long size) {
122     return (int)(size / getFormat().PAGE_SIZE);
123   }
124 
125   /**
126    * Returns the offset for a page within the file.
127    */
128   private long getPageOffset(int pageNumber) {
129     return((long) pageNumber * (long) getFormat().PAGE_SIZE);
130   }
131   
132   /**
133    * Validates that the given pageNumber is valid for this database.
134    */
135   private void validatePageNumber(int pageNumber)
136     throws IOException
137   {
138     int nextPageNumber = getNextPageNumber(_channel.size());
139     if((pageNumber <= INVALID_PAGE_NUMBER) || (pageNumber >= nextPageNumber)) {
140       throw new IllegalStateException("invalid page number " + pageNumber);
141     }
142   }
143   
144   /**
145    * @param buffer Buffer to read the page into
146    * @param pageNumber Number of the page to read in (starting at 0)
147    */
148   public void readPage(ByteBuffer buffer, int pageNumber)
149     throws IOException
150   {
151     validatePageNumber(pageNumber);
152     if (LOG.isDebugEnabled()) {
153       LOG.debug("Reading in page " + Integer.toHexString(pageNumber));
154     }
155     buffer.clear();
156     int bytesRead = _channel.read(
157         buffer, (long) pageNumber * (long) getFormat().PAGE_SIZE);
158     buffer.flip();
159     if(bytesRead != getFormat().PAGE_SIZE) {
160       throw new IOException("Failed attempting to read " +
161                             getFormat().PAGE_SIZE + " bytes from page " +
162                             pageNumber + ", only read " + bytesRead);
163     }
164   }
165   
166   /**
167    * Write a page to disk
168    * @param page Page to write
169    * @param pageNumber Page number to write the page to
170    */
171   public void writePage(ByteBuffer page, int pageNumber) throws IOException {
172     writePage(page, pageNumber, 0);
173   }
174   
175   /**
176    * Write a page (or part of a page) to disk
177    * @param page Page to write
178    * @param pageNumber Page number to write the page to
179    * @param pageOffset offset within the page at which to start writing the
180    *                   page data
181    */
182   public void writePage(ByteBuffer page, int pageNumber,
183                         int pageOffset)
184     throws IOException
185   {
186     validatePageNumber(pageNumber);
187     
188     page.rewind();
189 
190     if((page.remaining() - pageOffset) > getFormat().PAGE_SIZE) {
191       throw new IllegalArgumentException(
192           "Page buffer is too large, size " + (page.remaining() - pageOffset));
193     }
194     
195     page.position(pageOffset);
196     _channel.write(page, (getPageOffset(pageNumber) + pageOffset));
197     if(_autoSync) {
198       flush();
199     }
200   }
201   
202   /**
203    * Write a page to disk as a new page, appending it to the database
204    * @param page Page to write
205    * @return Page number at which the page was written
206    */
207   public int writeNewPage(ByteBuffer page) throws IOException
208   {
209     long size = _channel.size();
210     if(size >= getFormat().MAX_DATABASE_SIZE) {
211       throw new IOException("Database is at maximum size " +
212                             getFormat().MAX_DATABASE_SIZE);
213     }
214     if((size % getFormat().PAGE_SIZE) != 0L) {
215       throw new IOException("Database corrupted, file size " + size +
216                             " is not multiple of page size " +
217                             getFormat().PAGE_SIZE);
218     }
219     
220     page.rewind();
221 
222     if(page.remaining() > getFormat().PAGE_SIZE) {
223       throw new IllegalArgumentException("Page buffer is too large, size " +
224                                          page.remaining());
225     }
226     
227     // push the buffer to the end of the page, so that a full page's worth of
228     // data is written regardless of the incoming buffer size (we use a tiny
229     // buffer in allocateNewPage)
230     long offset = size + (getFormat().PAGE_SIZE - page.remaining());
231     _channel.write(page, offset);
232     int pageNumber = getNextPageNumber(size);
233     _globalUsageMap.removePageNumber(pageNumber);  //force is done here
234     return pageNumber;
235   }
236 
237   /**
238    * Allocates a new page in the database.  Data in the page is undefined
239    * until it is written in a call to {@link #writePage(ByteBuffer,int)}.
240    */
241   public int allocateNewPage() throws IOException {
242     // this will force the file to be extended with mostly undefined bytes
243     return writeNewPage(_forceBytes);
244   }
245 
246   /**
247    * Deallocate a previously used page in the database.
248    */
249   public void deallocatePage(int pageNumber) throws IOException {
250     validatePageNumber(pageNumber);
251     
252     // don't write the whole page, just wipe out the header (which should be
253     // enough to let us know if we accidentally try to use an invalid page)
254     _invalidPageBytes.rewind();
255     _channel.write(_invalidPageBytes, getPageOffset(pageNumber));
256     
257     _globalUsageMap.addPageNumber(pageNumber);  //force is done here
258   }
259   
260   /**
261    * @return A newly-allocated buffer that can be passed to readPage
262    */
263   public ByteBuffer createPageBuffer() {
264     return createBuffer(getFormat().PAGE_SIZE);
265   }
266 
267   /**
268    * @return A newly-allocated buffer of the given size and LITTLE_ENDIAN byte
269    *         order
270    */
271   public ByteBuffer createBuffer(int size) {
272     return createBuffer(size, ByteOrder.LITTLE_ENDIAN);
273   }
274   
275   /**
276    * @return A newly-allocated buffer of the given size and byte order
277    */
278   public ByteBuffer createBuffer(int size, ByteOrder order) {
279     ByteBuffer rtn = ByteBuffer.allocate(size);
280     rtn.order(order);
281     return rtn;
282   }
283   
284   public void flush() throws IOException {
285     _channel.force(true);
286   }
287   
288   public void close() throws IOException {
289     flush();
290     _channel.close();
291   }
292   
293   public boolean isOpen() {
294     return _channel.isOpen();
295   }
296 
297   /**
298    * @return a duplicate of the current buffer narrowed to the given position
299    *         and limit.  mark will be set at the current position.
300    */
301   public static ByteBuffer narrowBuffer(ByteBuffer buffer, int position,
302                                         int limit)
303   {
304     return (ByteBuffer)buffer.duplicate()
305       .order(buffer.order())
306       .clear()
307       .limit(limit)
308       .position(position)
309       .mark();
310   }
311   
312 }