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    /** lowest page the re-use rule may hand out, which is where its search
49        starts and the lowest a page given back can move it to.  page 0 holds
50        the header and page 1 the global usage map, and a file written by
51        another tool can mark either of them free */
52    private static final int FIRST_REUSABLE_PAGE_NUMBER =
53      PAGE_GLOBAL_USAGE_MAP + 1;
54  
55    /** Channel containing the database */
56    private final FileChannel _channel;
57    /** whether or not the _channel should be closed by this class */
58    private final boolean _closeChannel;
59    /** Format of the database in the channel */
60    private final JetFormat _format;
61    /** whether or not to force all writes to disk immediately */
62    private final  boolean _autoSync;
63    /** buffer used when deallocating old pages.  data pages generally have 4
64        interesting bytes at the beginning which we want to reset. */
65    private final ByteBuffer _invalidPageBytes =
66      ByteBuffer.wrap(INVALID_PAGE_BYTE_HEADER);
67    /** dummy buffer used when allocating new pages */
68    private final ByteBuffer _forceBytes = ByteBuffer.allocate(1);
69    /** Tracks free pages in the database. */
70    private UsageMap _globalUsageMap;
71    /** whether a page which was given back can be handed out again */
72    private boolean _reuseFreePages;
73    /** the lowest page number the re-use rule has still to look at.  Without it
74        every allocation walks the free pages from the start of the database */
75    private int _reuseSearchStart = FIRST_REUSABLE_PAGE_NUMBER;
76    /** handler for the current database encoding type */
77    private CodecHandler _codecHandler = DefaultCodecProvider.DUMMY_HANDLER;
78    /** temp page buffer used when pages cannot be partially encoded */
79    private TempPageHolder _fullPageEncodeBufferH;
80    private TempBufferHolder _tempDecodeBufferH;
81    private int _writeCount;
82  
83    /**
84     * Only used by unit tests
85     */
86    protected PageChannel(boolean testing) {
87      if(!testing) {
88        throw new IllegalArgumentException();
89      }
90      _channel = null;
91      _closeChannel = false;
92      _format = JetFormat.VERSION_4;
93      _autoSync = false;
94    }
95  
96    /**
97     * @param channel Channel containing the database
98     * @param format Format of the database in the channel
99     */
100   public PageChannel(FileChannel channel, boolean closeChannel,
101                      JetFormat format, boolean autoSync)
102   {
103     _channel = channel;
104     _closeChannel = closeChannel;
105     _format = format;
106     _autoSync = autoSync;
107   }
108 
109   /**
110    * Does second-stage initialization, must be called after construction.
111    */
112   public void initialize(DatabaseImpl database, CodecProvider codecProvider)
113     throws IOException
114   {
115     // initialize page en/decoding support
116     _codecHandler = codecProvider.createHandler(this, database.getCharset());
117     if(!_codecHandler.canEncodePartialPage()) {
118       _fullPageEncodeBufferH =
119         TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
120     }
121     if(!_codecHandler.canDecodeInline()) {
122       _tempDecodeBufferH = TempBufferHolder.newHolder(
123           TempBufferHolder.Type.SOFT, true);
124     }
125 
126     // note the global usage map is a special map where any page outside of
127     // the current range is assumed to be "on"
128     _globalUsageMap = UsageMap.read(database, PAGE_GLOBAL_USAGE_MAP,
129                                     ROW_GLOBAL_USAGE_MAP, true);
130   }
131 
132   public JetFormat getFormat() {
133     return _format;
134   }
135 
136   public boolean isAutoSync() {
137     return _autoSync;
138   }
139 
140   public boolean isReuseFreePages() {
141     return _reuseFreePages;
142   }
143 
144   public void setReuseFreePages(boolean reuseFreePages) {
145     _reuseFreePages = reuseFreePages;
146   }
147 
148   /**
149    * Begins a "logical" write operation.  See {@link #finishWrite} for more
150    * details.
151    */
152   public void startWrite() {
153     ++_writeCount;
154   }
155 
156   /**
157    * Begins an exclusive "logical" write operation (throws an exception if
158    * another write operation is outstanding).  See {@link #finishWrite} for
159    * more details.
160    */
161   public void startExclusiveWrite() {
162     if(_writeCount != 0) {
163       throw new IllegalArgumentException(
164           "Another write operation is currently in progress");
165     }
166     startWrite();
167   }
168 
169   /**
170    * Completes a "logical" write operation.  This method should be called in
171    * finally block which wraps a logical write operation (which is preceded by
172    * a {@link #startWrite} call).  Logical write operations may be nested.  If
173    * the database is configured for "auto-sync", the channel will be flushed
174    * when the outermost operation is complete,
175    */
176   public void finishWrite() throws IOException {
177     assertWriting();
178     if((--_writeCount == 0) && _autoSync) {
179       flush();
180     }
181   }
182 
183   /**
184    * Returns {@code true} if a logical write operation is in progress, {@code
185    * false} otherwise.
186    */
187   public boolean isWriting() {
188     return(_writeCount > 0);
189   }
190 
191   /**
192    * Asserts that a write operation is in progress.
193    */
194   private void assertWriting() {
195     if(!isWriting()) {
196       throw new IllegalStateException("No write operation in progress");
197     }
198   }
199 
200   /**
201    * Returns the next page number based on the given file size.
202    */
203   private int getNextPageNumber(long size) {
204     return (int)(size / getFormat().PAGE_SIZE);
205   }
206 
207   /**
208    * Returns the size of the file, which the number of the page past the end
209    * of it is worked out from.
210    *
211    * @throws IOException if the file does not hold whole pages, because the
212    *         page number that size gives is one the file does not reach
213    */
214   private long getWholePageFileSize() throws IOException {
215     long size = _channel.size();
216     if((size % getFormat().PAGE_SIZE) != 0L) {
217       throw new IOException("Database corrupted, file size " + size +
218                             " is not multiple of page size " +
219                             getFormat().PAGE_SIZE);
220     }
221     return size;
222   }
223 
224   /**
225    * Returns the offset for a page within the file.
226    */
227   private long getPageOffset(int pageNumber) {
228     return((long) pageNumber * (long) getFormat().PAGE_SIZE);
229   }
230 
231   /**
232    * Validates that the given pageNumber is valid for this database.  A page
233    * number read from a corrupt file can name a page the file does not hold,
234    * so this is an IOException rather than a runtime exception.
235    */
236   private void validatePageNumber(int pageNumber)
237     throws IOException
238   {
239     int nextPageNumber = getNextPageNumber(_channel.size());
240     if((pageNumber <= INVALID_PAGE_NUMBER) || (pageNumber >= nextPageNumber)) {
241       throw new IOException("invalid page number " + pageNumber);
242     }
243   }
244 
245   /**
246    * Reads one whole page into the given buffer at its current position.  A
247    * channel may return fewer bytes than were asked for, so this repeats until
248    * the page is complete.
249    */
250   private void readFully(ByteBuffer buffer, long offset, int pageNumber)
251     throws IOException
252   {
253     int pageSize = getFormat().PAGE_SIZE;
254     int bytesRead = 0;
255     while(bytesRead < pageSize) {
256       int read = _channel.read(buffer, offset + bytesRead);
257       if(read <= 0) {
258         // the file ends before the page does, or the buffer cannot hold a
259         // whole page.  Without this the loop would make no progress
260         throw new IOException("Failed attempting to read " + pageSize +
261                               " bytes from page " + pageNumber +
262                               ", only read " + bytesRead);
263       }
264       bytesRead += read;
265     }
266   }
267 
268   /**
269    * Writes the remainder of the given buffer at the given offset.  A channel
270    * may take fewer bytes than were given to it, so this repeats until the
271    * buffer is empty.
272    */
273   private void writeFully(ByteBuffer buffer, long offset, int pageNumber)
274     throws IOException
275   {
276     int writeLen = buffer.remaining();
277     int bytesWritten = 0;
278     while(bytesWritten < writeLen) {
279       int written = _channel.write(buffer, offset + bytesWritten);
280       if(written <= 0) {
281         // the channel is making no progress, and without this the loop would
282         // not end
283         throw new IOException("Failed attempting to write " + writeLen +
284                               " bytes to page " + pageNumber +
285                               ", only wrote " + bytesWritten);
286       }
287       bytesWritten += written;
288     }
289   }
290 
291   /**
292    * @param buffer Buffer to read the page into
293    * @param pageNumber Number of the page to read in (starting at 0)
294    */
295   public void readPage(ByteBuffer buffer, int pageNumber)
296     throws IOException
297   {
298     if(pageNumber == 0) {
299       readRootPage(buffer);
300       return;
301     }
302 
303     validatePageNumber(pageNumber);
304 
305     ByteBuffer inPage = buffer;
306     ByteBuffer outPage = buffer;
307     if(!_codecHandler.canDecodeInline()) {
308       inPage = _tempDecodeBufferH.getPageBuffer(this);
309       outPage.clear();
310     }
311 
312     inPage.clear();
313     readFully(inPage, (long) pageNumber * (long) getFormat().PAGE_SIZE,
314               pageNumber);
315     inPage.flip();
316 
317     _codecHandler.decodePage(inPage, outPage, pageNumber);
318   }
319 
320   /**
321    * @param buffer Buffer to read the root page into
322    */
323   public void readRootPage(ByteBuffer buffer)
324     throws IOException
325   {
326     // special method for reading root page, can be done before PageChannel is
327     // fully initialized
328     buffer.clear();
329     readFully(buffer, 0L, 0);
330     buffer.flip();
331 
332     // de-mask header (note, page 0 never has additional encoding)
333     applyHeaderMask(buffer);
334   }
335 
336   /**
337    * Write a page to disk
338    * @param page Page to write
339    * @param pageNumber Page number to write the page to
340    */
341   public void writePage(ByteBuffer page, int pageNumber) throws IOException {
342     writePage(page, pageNumber, 0);
343   }
344 
345   /**
346    * Write a page (or part of a page) to disk
347    * @param page Page to write
348    * @param pageNumber Page number to write the page to
349    * @param pageOffset offset within the page at which to start writing the
350    *                   page data
351    */
352   public void writePage(ByteBuffer page, int pageNumber, int pageOffset)
353     throws IOException
354   {
355     assertWriting();
356     validatePageNumber(pageNumber);
357 
358     page.rewind().position(pageOffset);
359 
360     int writeLen = page.remaining();
361     if((writeLen + pageOffset) > getFormat().PAGE_SIZE) {
362       throw new IllegalArgumentException(
363           "Page buffer is too large, size " + (writeLen + pageOffset));
364     }
365 
366     ByteBuffer encodedPage = page;
367     if(pageNumber == 0) {
368       // re-mask header
369       applyHeaderMask(page);
370     } else {
371 
372       if(!_codecHandler.canEncodePartialPage()) {
373         if((pageOffset > 0) && (writeLen < getFormat().PAGE_SIZE)) {
374 
375           // current codec handler cannot encode part of a page, so need to
376           // copy the modified part into the current page contents in a temp
377           // buffer so that we can encode the entire page
378           ByteBuffer fullPage = _fullPageEncodeBufferH.setPage(
379               this, pageNumber);
380 
381           // copy the modified part to the full page
382           fullPage.position(pageOffset);
383           fullPage.put(page);
384           fullPage.rewind();
385 
386           // reset so we can write the whole page
387           page = fullPage;
388           pageOffset = 0;
389           writeLen = getFormat().PAGE_SIZE;
390 
391         } else {
392 
393           _fullPageEncodeBufferH.possiblyInvalidate(pageNumber, null);
394         }
395       }
396 
397       // re-encode page
398       encodedPage = _codecHandler.encodePage(page, pageNumber, pageOffset);
399 
400       // reset position/limit in case they were affected by encoding
401       encodedPage.position(pageOffset).limit(pageOffset + writeLen);
402     }
403 
404     try {
405       writeFully(encodedPage, getPageOffset(pageNumber) + pageOffset,
406                  pageNumber);
407     } finally {
408       if(pageNumber == 0) {
409         // de-mask header
410         applyHeaderMask(page);
411       }
412     }
413   }
414 
415   /**
416    * Allocates a new page in the database.  Data in the page is undefined
417    * until it is written in a call to {@link #writePage(ByteBuffer,int)}.
418    */
419   public int allocateNewPage() throws IOException {
420     assertWriting();
421 
422     long size = getWholePageFileSize();
423 
424     int pageNumber = getNextPageNumber(size);
425 
426     if(_reuseFreePages) {
427       int freePageNumber = findFreePage(pageNumber);
428       if(freePageNumber != INVALID_PAGE_NUMBER) {
429         // the file does not grow, so there is nothing to write here.  the
430         // page keeps its old bytes until the caller writes it, which every
431         // caller does before anything reads the page
432         _globalUsageMap.removePageNumber(freePageNumber);
433         return freePageNumber;
434       }
435     }
436 
437     // only growing the file can reach the size limit, so this is checked
438     // after the re-use rule has had its turn.  a database at the limit which
439     // holds free pages is the case the rule exists for
440     if(size >= getFormat().MAX_DATABASE_SIZE) {
441       throw new IOException("Database is at maximum size " +
442                             getFormat().MAX_DATABASE_SIZE);
443     }
444 
445     _forceBytes.rewind();
446 
447     // push the buffer to the end of the page, so that a full page's worth of
448     // data is written.  this will force the file to be extended with mostly
449     // undefined bytes
450     int pageOffset = (getFormat().PAGE_SIZE - _forceBytes.remaining());
451     long offset = size + pageOffset;
452 
453     // since we are just allocating page space at this point and not writing
454     // meaningful data, we do _not_ encode the page.
455     writeFully(_forceBytes, offset, pageNumber);
456 
457     _globalUsageMap.removePageNumber(pageNumber);
458     return pageNumber;
459   }
460 
461   /**
462    * Returns the lowest free page the file holds, or {@link
463    * #INVALID_PAGE_NUMBER} where it holds none.
464    *
465    * @param nextPageNumber the page which extending the file would give out,
466    *                       which bounds the search.  The global usage map
467    *                       covers a fixed range of pages, 512 for an inline
468    *                       map and 32736 for each page of a reference map,
469    *                       and it marks the pages of that range which the
470    *                       file does not reach free.  A search which does not
471    *                       stop at the end of the file gives out one of those
472    */
473   private int findFreePage(int nextPageNumber) {
474     // the search starts at FIRST_REUSABLE_PAGE_NUMBER and never moves below
475     // it, so the low test here is the no free page answer
476     int pageNumber = _globalUsageMap.getNextPageNumberFrom(_reuseSearchStart);
477     if((pageNumber < FIRST_REUSABLE_PAGE_NUMBER) ||
478        (pageNumber >= nextPageNumber)) {
479       // the caller now extends the file and takes nextPageNumber, so the
480       // search goes above it.  the global usage map still calls that page
481       // free until the caller removes it, and GlobalReferenceHandler can
482       // defer that removal past another allocation, which would hand the page
483       // out twice.  deallocatePage moves the search back down
484       _reuseSearchStart = nextPageNumber + 1;
485       return INVALID_PAGE_NUMBER;
486     }
487     _reuseSearchStart = pageNumber + 1;
488     return pageNumber;
489   }
490 
491   /**
492    * Shortens the file to drop the run of free pages at the end of it, and
493    * returns the number of pages it dropped.  A file can be shortened only at
494    * its end, so this is the whole of what can be given back without rewriting
495    * the file.
496    */
497   public int truncateFreeSpace() throws IOException {
498     assertWriting();
499 
500     int nextPageNumber = getNextPageNumber(getWholePageFileSize());
501     int firstFreePageNumber = nextPageNumber;
502     while((firstFreePageNumber > FIRST_REUSABLE_PAGE_NUMBER) &&
503           _globalUsageMap.holdsPageNumber(firstFreePageNumber - 1)) {
504       --firstFreePageNumber;
505     }
506 
507     int pageCount = nextPageNumber - firstFreePageNumber;
508     if(pageCount == 0) {
509       return 0;
510     }
511 
512     // a page buffer which still names a dropped page would write it back and
513     // grow the file again
514     if(_fullPageEncodeBufferH != null) {
515       _fullPageEncodeBufferH.invalidate();
516     }
517 
518     // the dropped pages stay in the global usage map, which says they are
519     // free, and so does a file which does not hold them.  a page number which
520     // outlives the truncate fails in validatePageNumber rather than reading
521     // another page's bytes
522     _channel.truncate(getPageOffset(firstFreePageNumber));
523 
524     return pageCount;
525   }
526 
527   /**
528    * Deallocate a previously used page in the database.
529    */
530   public void deallocatePage(int pageNumber) throws IOException {
531     assertWriting();
532 
533     validatePageNumber(pageNumber);
534 
535     // don't write the whole page, just wipe out the header (which should be
536     // enough to let us know if we accidentally try to use an invalid page)
537     _invalidPageBytes.rewind();
538     _channel.write(_invalidPageBytes, getPageOffset(pageNumber));
539 
540     _globalUsageMap.addPageNumber(pageNumber);  //force is done here
541 
542     // the page can go to a new owner right away, and a buffer which still
543     // holds it would merge a partial write into the bytes of the old owner
544     // and write those back
545     if(_fullPageEncodeBufferH != null) {
546       _fullPageEncodeBufferH.possiblyInvalidate(pageNumber, null);
547     }
548 
549     if((pageNumber < _reuseSearchStart) &&
550        (pageNumber >= FIRST_REUSABLE_PAGE_NUMBER)) {
551       // the re-use rule takes the lowest free page, so a page given back
552       // below where it is looking moves it back down
553       _reuseSearchStart = pageNumber;
554     }
555   }
556 
557   /**
558    * @return A newly-allocated buffer that can be passed to readPage
559    */
560   public ByteBuffer createPageBuffer() {
561     return createBuffer(getFormat().PAGE_SIZE);
562   }
563 
564   /**
565    * @return A newly-allocated buffer of the given size and DEFAULT_BYTE_ORDER
566    *         byte order
567    */
568   public static ByteBuffer createBuffer(int size) {
569     return createBuffer(size, DEFAULT_BYTE_ORDER);
570   }
571 
572   /**
573    * @return A newly-allocated buffer of the given size and byte order
574    */
575   public static ByteBuffer createBuffer(int size, ByteOrder order) {
576     return ByteBuffer.allocate(size).order(order);
577   }
578 
579   @Override
580   public void flush() throws IOException {
581     _channel.force(true);
582   }
583 
584   @Override
585   public void close() throws IOException {
586     flush();
587     if(_closeChannel) {
588       _channel.close();
589     }
590   }
591 
592   @Override
593   public boolean isOpen() {
594     return _channel.isOpen();
595   }
596 
597   /**
598    * Applies the XOR mask to the database header in the given buffer.
599    */
600   private void applyHeaderMask(ByteBuffer buffer) {
601       // de/re-obfuscate the header
602       byte[] headerMask = _format.HEADER_MASK;
603       for(int idx = 0; idx < headerMask.length; ++idx) {
604         int pos = idx + _format.OFFSET_MASKED_HEADER;
605         byte b = (byte)(buffer.get(pos) ^ headerMask[idx]);
606         buffer.put(pos, b);
607       }
608   }
609 
610   /**
611    * @return a duplicate of the current buffer narrowed to the given position
612    *         and limit.  mark will be set at the current position.
613    */
614   public static ByteBuffer narrowBuffer(ByteBuffer buffer, int position,
615                                         int limit)
616   {
617     return buffer.duplicate()
618       .order(buffer.order())
619       .clear()
620       .limit(limit)
621       .position(position)
622       .mark();
623   }
624 
625   /**
626    * Returns a ByteBuffer wrapping the given bytes and configured with the
627    * default byte order.
628    */
629   public static ByteBuffer wrap(byte[] bytes) {
630     return ByteBuffer.wrap(bytes).order(DEFAULT_BYTE_ORDER);
631   }
632 }