View Javadoc
1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.Flushable;
20  import java.io.IOException;
21  import java.nio.ByteBuffer;
22  import java.nio.ByteOrder;
23  import java.nio.channels.Channel;
24  import java.nio.channels.FileChannel;
25  
26  
27  /**
28   * Reads and writes individual pages in a database file
29   * @author Tim McCune
30   */
31  public class PageChannel implements Channel, Flushable {
32  
33    static final int INVALID_PAGE_NUMBER = -1;
34  
35    /** default byte order of access mdb files */
36    public static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
37  
38    /** invalid page header, used when deallocating old pages.  data pages
39        generally have 4 interesting bytes at the beginning which we want to
40        reset. */
41    private static final byte[] INVALID_PAGE_BYTE_HEADER =
42      new byte[]{PageTypes.INVALID, (byte)0, (byte)0, (byte)0};
43  
44    /** Global usage map always lives on page 1 */
45    static final int PAGE_GLOBAL_USAGE_MAP = 1;
46    /** Global usage map always lives at row 0 */
47    static final int ROW_GLOBAL_USAGE_MAP = 0;
48  
49    /** Channel containing the database */
50    private final FileChannel _channel;
51    /** whether or not the _channel should be closed by this class */
52    private final boolean _closeChannel;
53    /** Format of the database in the channel */
54    private final JetFormat _format;
55    /** whether or not to force all writes to disk immediately */
56    private final  boolean _autoSync;
57    /** buffer used when deallocating old pages.  data pages generally have 4
58        interesting bytes at the beginning which we want to reset. */
59    private final ByteBuffer _invalidPageBytes =
60      ByteBuffer.wrap(INVALID_PAGE_BYTE_HEADER);
61    /** dummy buffer used when allocating new pages */
62    private final ByteBuffer _forceBytes = ByteBuffer.allocate(1);
63    /** Tracks free pages in the database. */
64    private UsageMap _globalUsageMap;
65    /** handler for the current database encoding type */
66    private CodecHandler _codecHandler = DefaultCodecProvider.DUMMY_HANDLER;
67    /** temp page buffer used when pages cannot be partially encoded */
68    private TempPageHolder _fullPageEncodeBufferH;
69    private TempBufferHolder _tempDecodeBufferH;
70    private int _writeCount;
71  
72    /**
73     * Only used by unit tests
74     */
75    protected PageChannel(boolean testing) {
76      if(!testing) {
77        throw new IllegalArgumentException();
78      }
79      _channel = null;
80      _closeChannel = false;
81      _format = JetFormat.VERSION_4;
82      _autoSync = false;
83    }
84  
85    /**
86     * @param channel Channel containing the database
87     * @param format Format of the database in the channel
88     */
89    public PageChannel(FileChannel channel, boolean closeChannel,
90                       JetFormat format, boolean autoSync)
91    {
92      _channel = channel;
93      _closeChannel = closeChannel;
94      _format = format;
95      _autoSync = autoSync;
96    }
97  
98    /**
99     * Does second-stage initialization, must be called after construction.
100    */
101   public void initialize(DatabaseImpl database, CodecProvider codecProvider)
102     throws IOException
103   {
104     // initialize page en/decoding support
105     _codecHandler = codecProvider.createHandler(this, database.getCharset());
106     if(!_codecHandler.canEncodePartialPage()) {
107       _fullPageEncodeBufferH =
108         TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
109     }
110     if(!_codecHandler.canDecodeInline()) {
111       _tempDecodeBufferH = TempBufferHolder.newHolder(
112           TempBufferHolder.Type.SOFT, true);
113     }
114 
115     // note the global usage map is a special map where any page outside of
116     // the current range is assumed to be "on"
117     _globalUsageMap = UsageMap.read(database, PAGE_GLOBAL_USAGE_MAP,
118                                     ROW_GLOBAL_USAGE_MAP, true);
119   }
120 
121   public JetFormat getFormat() {
122     return _format;
123   }
124 
125   public boolean isAutoSync() {
126     return _autoSync;
127   }
128 
129   /**
130    * Begins a "logical" write operation.  See {@link #finishWrite} for more
131    * details.
132    */
133   public void startWrite() {
134     ++_writeCount;
135   }
136 
137   /**
138    * Begins an exclusive "logical" write operation (throws an exception if
139    * another write operation is outstanding).  See {@link #finishWrite} for
140    * more details.
141    */
142   public void startExclusiveWrite() {
143     if(_writeCount != 0) {
144       throw new IllegalArgumentException(
145           "Another write operation is currently in progress");
146     }
147     startWrite();
148   }
149 
150   /**
151    * Completes a "logical" write operation.  This method should be called in
152    * finally block which wraps a logical write operation (which is preceded by
153    * a {@link #startWrite} call).  Logical write operations may be nested.  If
154    * the database is configured for "auto-sync", the channel will be flushed
155    * when the outermost operation is complete,
156    */
157   public void finishWrite() throws IOException {
158     assertWriting();
159     if((--_writeCount == 0) && _autoSync) {
160       flush();
161     }
162   }
163 
164   /**
165    * Returns {@code true} if a logical write operation is in progress, {@code
166    * false} otherwise.
167    */
168   public boolean isWriting() {
169     return(_writeCount > 0);
170   }
171 
172   /**
173    * Asserts that a write operation is in progress.
174    */
175   private void assertWriting() {
176     if(!isWriting()) {
177       throw new IllegalStateException("No write operation in progress");
178     }
179   }
180 
181   /**
182    * Returns the next page number based on the given file size.
183    */
184   private int getNextPageNumber(long size) {
185     return (int)(size / getFormat().PAGE_SIZE);
186   }
187 
188   /**
189    * Returns the offset for a page within the file.
190    */
191   private long getPageOffset(int pageNumber) {
192     return((long) pageNumber * (long) getFormat().PAGE_SIZE);
193   }
194 
195   /**
196    * Validates that the given pageNumber is valid for this database.
197    */
198   private void validatePageNumber(int pageNumber)
199     throws IOException
200   {
201     int nextPageNumber = getNextPageNumber(_channel.size());
202     if((pageNumber <= INVALID_PAGE_NUMBER) || (pageNumber >= nextPageNumber)) {
203       throw new IllegalStateException("invalid page number " + pageNumber);
204     }
205   }
206 
207   /**
208    * @param buffer Buffer to read the page into
209    * @param pageNumber Number of the page to read in (starting at 0)
210    */
211   public void readPage(ByteBuffer buffer, int pageNumber)
212     throws IOException
213   {
214     if(pageNumber == 0) {
215       readRootPage(buffer);
216       return;
217     }
218 
219     validatePageNumber(pageNumber);
220 
221     ByteBuffer inPage = buffer;
222     ByteBuffer outPage = buffer;
223     if(!_codecHandler.canDecodeInline()) {
224       inPage = _tempDecodeBufferH.getPageBuffer(this);
225       outPage.clear();
226     }
227 
228     inPage.clear();
229     int bytesRead = _channel.read(
230         inPage, (long) pageNumber * (long) getFormat().PAGE_SIZE);
231     inPage.flip();
232     if(bytesRead != getFormat().PAGE_SIZE) {
233       throw new IOException("Failed attempting to read " +
234                             getFormat().PAGE_SIZE + " bytes from page " +
235                             pageNumber + ", only read " + bytesRead);
236     }
237 
238     _codecHandler.decodePage(inPage, outPage, pageNumber);
239   }
240 
241   /**
242    * @param buffer Buffer to read the root page into
243    */
244   public void readRootPage(ByteBuffer buffer)
245     throws IOException
246   {
247     // special method for reading root page, can be done before PageChannel is
248     // fully initialized
249     buffer.clear();
250     int bytesRead = _channel.read(buffer, 0L);
251     buffer.flip();
252     if(bytesRead != getFormat().PAGE_SIZE) {
253       throw new IOException("Failed attempting to read " +
254                             getFormat().PAGE_SIZE + " bytes from page " +
255                             0 + ", only read " + bytesRead);
256     }
257 
258     // de-mask header (note, page 0 never has additional encoding)
259     applyHeaderMask(buffer);
260   }
261 
262   /**
263    * Write a page to disk
264    * @param page Page to write
265    * @param pageNumber Page number to write the page to
266    */
267   public void writePage(ByteBuffer page, int pageNumber) throws IOException {
268     writePage(page, pageNumber, 0);
269   }
270 
271   /**
272    * Write a page (or part of a page) to disk
273    * @param page Page to write
274    * @param pageNumber Page number to write the page to
275    * @param pageOffset offset within the page at which to start writing the
276    *                   page data
277    */
278   public void writePage(ByteBuffer page, int pageNumber, int pageOffset)
279     throws IOException
280   {
281     assertWriting();
282     validatePageNumber(pageNumber);
283 
284     page.rewind().position(pageOffset);
285 
286     int writeLen = page.remaining();
287     if((writeLen + pageOffset) > getFormat().PAGE_SIZE) {
288       throw new IllegalArgumentException(
289           "Page buffer is too large, size " + (writeLen + pageOffset));
290     }
291 
292     ByteBuffer encodedPage = page;
293     if(pageNumber == 0) {
294       // re-mask header
295       applyHeaderMask(page);
296     } else {
297 
298       if(!_codecHandler.canEncodePartialPage()) {
299         if((pageOffset > 0) && (writeLen < getFormat().PAGE_SIZE)) {
300 
301           // current codec handler cannot encode part of a page, so need to
302           // copy the modified part into the current page contents in a temp
303           // buffer so that we can encode the entire page
304           ByteBuffer fullPage = _fullPageEncodeBufferH.setPage(
305               this, pageNumber);
306 
307           // copy the modified part to the full page
308           fullPage.position(pageOffset);
309           fullPage.put(page);
310           fullPage.rewind();
311 
312           // reset so we can write the whole page
313           page = fullPage;
314           pageOffset = 0;
315           writeLen = getFormat().PAGE_SIZE;
316 
317         } else {
318 
319           _fullPageEncodeBufferH.possiblyInvalidate(pageNumber, null);
320         }
321       }
322 
323       // re-encode page
324       encodedPage = _codecHandler.encodePage(page, pageNumber, pageOffset);
325 
326       // reset position/limit in case they were affected by encoding
327       encodedPage.position(pageOffset).limit(pageOffset + writeLen);
328     }
329 
330     try {
331       _channel.write(encodedPage, (getPageOffset(pageNumber) + pageOffset));
332     } finally {
333       if(pageNumber == 0) {
334         // de-mask header
335         applyHeaderMask(page);
336       }
337     }
338   }
339 
340   /**
341    * Allocates a new page in the database.  Data in the page is undefined
342    * until it is written in a call to {@link #writePage(ByteBuffer,int)}.
343    */
344   public int allocateNewPage() throws IOException {
345     assertWriting();
346 
347     // this will force the file to be extended with mostly undefined bytes
348     long size = _channel.size();
349     if(size >= getFormat().MAX_DATABASE_SIZE) {
350       throw new IOException("Database is at maximum size " +
351                             getFormat().MAX_DATABASE_SIZE);
352     }
353     if((size % getFormat().PAGE_SIZE) != 0L) {
354       throw new IOException("Database corrupted, file size " + size +
355                             " is not multiple of page size " +
356                             getFormat().PAGE_SIZE);
357     }
358 
359     _forceBytes.rewind();
360 
361     // push the buffer to the end of the page, so that a full page's worth of
362     // data is written
363     int pageOffset = (getFormat().PAGE_SIZE - _forceBytes.remaining());
364     long offset = size + pageOffset;
365     int pageNumber = getNextPageNumber(size);
366 
367     // since we are just allocating page space at this point and not writing
368     // meaningful data, we do _not_ encode the page.
369     _channel.write(_forceBytes, offset);
370 
371     _globalUsageMap.removePageNumber(pageNumber);
372     return pageNumber;
373   }
374 
375   /**
376    * Deallocate a previously used page in the database.
377    */
378   public void deallocatePage(int pageNumber) throws IOException {
379     assertWriting();
380 
381     validatePageNumber(pageNumber);
382 
383     // don't write the whole page, just wipe out the header (which should be
384     // enough to let us know if we accidentally try to use an invalid page)
385     _invalidPageBytes.rewind();
386     _channel.write(_invalidPageBytes, getPageOffset(pageNumber));
387 
388     _globalUsageMap.addPageNumber(pageNumber);  //force is done here
389   }
390 
391   /**
392    * @return A newly-allocated buffer that can be passed to readPage
393    */
394   public ByteBuffer createPageBuffer() {
395     return createBuffer(getFormat().PAGE_SIZE);
396   }
397 
398   /**
399    * @return A newly-allocated buffer of the given size and DEFAULT_BYTE_ORDER
400    *         byte order
401    */
402   public static ByteBuffer createBuffer(int size) {
403     return createBuffer(size, DEFAULT_BYTE_ORDER);
404   }
405 
406   /**
407    * @return A newly-allocated buffer of the given size and byte order
408    */
409   public static ByteBuffer createBuffer(int size, ByteOrder order) {
410     return ByteBuffer.allocate(size).order(order);
411   }
412 
413   @Override
414   public void flush() throws IOException {
415     _channel.force(true);
416   }
417 
418   @Override
419   public void close() throws IOException {
420     flush();
421     if(_closeChannel) {
422       _channel.close();
423     }
424   }
425 
426   @Override
427   public boolean isOpen() {
428     return _channel.isOpen();
429   }
430 
431   /**
432    * Applies the XOR mask to the database header in the given buffer.
433    */
434   private void applyHeaderMask(ByteBuffer buffer) {
435       // de/re-obfuscate the header
436       byte[] headerMask = _format.HEADER_MASK;
437       for(int idx = 0; idx < headerMask.length; ++idx) {
438         int pos = idx + _format.OFFSET_MASKED_HEADER;
439         byte b = (byte)(buffer.get(pos) ^ headerMask[idx]);
440         buffer.put(pos, b);
441       }
442   }
443 
444   /**
445    * @return a duplicate of the current buffer narrowed to the given position
446    *         and limit.  mark will be set at the current position.
447    */
448   public static ByteBuffer narrowBuffer(ByteBuffer buffer, int position,
449                                         int limit)
450   {
451     return (ByteBuffer)buffer.duplicate()
452       .order(buffer.order())
453       .clear()
454       .limit(limit)
455       .position(position)
456       .mark();
457   }
458 
459   /**
460    * Returns a ByteBuffer wrapping the given bytes and configured with the
461    * default byte order.
462    */
463   public static ByteBuffer wrap(byte[] bytes) {
464     return ByteBuffer.wrap(bytes).order(DEFAULT_BYTE_ORDER);
465   }
466 }