123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- /*
- Copyright (c) 2005 Health Market Science, Inc.
-
- This library is free software; you can redistribute it and/or
- modify it under the terms of the GNU Lesser General Public
- License as published by the Free Software Foundation; either
- version 2.1 of the License, or (at your option) any later version.
-
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public
- License along with this library; if not, write to the Free Software
- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- USA
-
- You can contact Health Market Science at info@healthmarketscience.com
- or at the following address:
-
- Health Market Science
- 2700 Horizon Drive
- Suite 200
- King of Prussia, PA 19406
- */
-
- package com.healthmarketscience.jackcess;
-
- import java.io.Flushable;
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.nio.ByteOrder;
- import java.nio.channels.Channel;
- import java.nio.channels.FileChannel;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
-
- /**
- * Reads and writes individual pages in a database file
- * @author Tim McCune
- */
- public class PageChannel implements Channel, Flushable {
-
- private static final Log LOG = LogFactory.getLog(PageChannel.class);
-
- static final int INVALID_PAGE_NUMBER = -1;
-
- static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
-
- /** invalid page header, used when deallocating old pages. data pages
- generally have 4 interesting bytes at the beginning which we want to
- reset. */
- private static final byte[] INVALID_PAGE_BYTE_HEADER =
- new byte[]{PageTypes.INVALID, (byte)0, (byte)0, (byte)0};
-
- /** Global usage map always lives on page 1 */
- static final int PAGE_GLOBAL_USAGE_MAP = 1;
- /** Global usage map always lives at row 0 */
- static final int ROW_GLOBAL_USAGE_MAP = 0;
-
- /** Channel containing the database */
- private final FileChannel _channel;
- /** Format of the database in the channel */
- private final JetFormat _format;
- /** whether or not to force all writes to disk immediately */
- private final boolean _autoSync;
- /** buffer used when deallocating old pages. data pages generally have 4
- interesting bytes at the beginning which we want to reset. */
- private final ByteBuffer _invalidPageBytes =
- ByteBuffer.wrap(INVALID_PAGE_BYTE_HEADER);
- /** dummy buffer used when allocating new pages */
- private final ByteBuffer _forceBytes = ByteBuffer.allocate(1);
- /** Tracks free pages in the database. */
- private UsageMap _globalUsageMap;
- /** handler for the current database encoding type */
- private CodecHandler _codecHandler = DefaultCodecProvider.DUMMY_HANDLER;
- /** temp page buffer used when pages cannot be partially encoded */
- private final TempPageHolder _fullPageEncodeBufferH =
- TempPageHolder.newHolder(TempBufferHolder.Type.SOFT);
-
- /**
- * @param channel Channel containing the database
- * @param format Format of the database in the channel
- */
- public PageChannel(FileChannel channel, JetFormat format, boolean autoSync)
- throws IOException
- {
- _channel = channel;
- _format = format;
- _autoSync = autoSync;
- }
-
- /**
- * Does second-stage initialization, must be called after construction.
- */
- public void initialize(Database database, CodecProvider codecProvider)
- throws IOException
- {
- // initialize page en/decoding support
- _codecHandler = codecProvider.createHandler(this, database.getCharset());
-
- // 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(database, PAGE_GLOBAL_USAGE_MAP,
- ROW_GLOBAL_USAGE_MAP, true);
- }
-
- /**
- * Only used by unit tests
- */
- PageChannel(boolean testing) {
- if(!testing) {
- throw new IllegalArgumentException();
- }
- _channel = null;
- _format = JetFormat.VERSION_4;
- _autoSync = false;
- }
-
- public JetFormat getFormat() {
- return _format;
- }
-
- public boolean isAutoSync() {
- return _autoSync;
- }
-
- /**
- * Returns the next page number based on the given file size.
- */
- private int getNextPageNumber(long size) {
- return (int)(size / getFormat().PAGE_SIZE);
- }
-
- /**
- * Returns the offset for a page within the file.
- */
- private long getPageOffset(int pageNumber) {
- return((long) pageNumber * (long) getFormat().PAGE_SIZE);
- }
-
- /**
- * Validates that the given pageNumber is valid for this database.
- */
- private void validatePageNumber(int pageNumber)
- throws IOException
- {
- int nextPageNumber = getNextPageNumber(_channel.size());
- if((pageNumber <= INVALID_PAGE_NUMBER) || (pageNumber >= nextPageNumber)) {
- throw new IllegalStateException("invalid page number " + pageNumber);
- }
- }
-
- /**
- * @param buffer Buffer to read the page into
- * @param pageNumber Number of the page to read in (starting at 0)
- */
- public void readPage(ByteBuffer buffer, int pageNumber)
- throws IOException
- {
- validatePageNumber(pageNumber);
- if (LOG.isDebugEnabled()) {
- LOG.debug("Reading in page " + Integer.toHexString(pageNumber));
- }
- buffer.clear();
- int bytesRead = _channel.read(
- buffer, (long) pageNumber * (long) getFormat().PAGE_SIZE);
- buffer.flip();
- if(bytesRead != getFormat().PAGE_SIZE) {
- throw new IOException("Failed attempting to read " +
- getFormat().PAGE_SIZE + " bytes from page " +
- pageNumber + ", only read " + bytesRead);
- }
-
- if(pageNumber == 0) {
- // de-mask header (note, page 0 never has additional encoding)
- applyHeaderMask(buffer);
- } else {
- _codecHandler.decodePage(buffer, pageNumber);
- }
- }
-
- /**
- * Write a page to disk
- * @param page Page to write
- * @param pageNumber Page number to write the page to
- */
- public void writePage(ByteBuffer page, int pageNumber) throws IOException {
- writePage(page, pageNumber, 0);
- }
-
- /**
- * Write a page (or part of a page) to disk
- * @param page Page to write
- * @param pageNumber Page number to write the page to
- * @param pageOffset offset within the page at which to start writing the
- * page data
- */
- public void writePage(ByteBuffer page, int pageNumber, int pageOffset)
- throws IOException
- {
- validatePageNumber(pageNumber);
-
- page.rewind().position(pageOffset);
-
- int writeLen = page.remaining();
- if((writeLen + pageOffset) > getFormat().PAGE_SIZE) {
- throw new IllegalArgumentException(
- "Page buffer is too large, size " + (writeLen + pageOffset));
- }
-
- ByteBuffer encodedPage = page;
- if(pageNumber == 0) {
- // re-mask header
- applyHeaderMask(page);
- } else {
-
- if(!_codecHandler.canEncodePartialPage()) {
- if((pageOffset > 0) && (writeLen < getFormat().PAGE_SIZE)) {
-
- // current codec handler cannot encode part of a page, so need to
- // copy the modified part into the current page contents in a temp
- // buffer so that we can encode the entire page
- ByteBuffer fullPage = _fullPageEncodeBufferH.setPage(
- this, pageNumber);
-
- // copy the modified part to the full page
- fullPage.position(pageOffset);
- fullPage.put(page);
- fullPage.rewind();
-
- // reset so we can write the whole page
- page = fullPage;
- pageOffset = 0;
-
- } else {
-
- _fullPageEncodeBufferH.possiblyInvalidate(pageNumber, null);
- }
- }
-
- // re-encode page
- encodedPage = _codecHandler.encodePage(page, pageNumber, pageOffset);
-
- // reset position/limit in case they were affected by encoding
- encodedPage.position(pageOffset).limit(pageOffset + writeLen);
- }
-
- try {
- _channel.write(encodedPage, (getPageOffset(pageNumber) + pageOffset));
- if(_autoSync) {
- flush();
- }
- } finally {
- if(pageNumber == 0) {
- // de-mask header
- applyHeaderMask(page);
- }
- }
- }
-
- /**
- * Allocates a new page in the database. Data in the page is undefined
- * until it is written in a call to {@link #writePage(ByteBuffer,int)}.
- */
- public int allocateNewPage() throws IOException {
- // this will force the file to be extended with mostly undefined bytes
- long size = _channel.size();
- if(size >= getFormat().MAX_DATABASE_SIZE) {
- throw new IOException("Database is at maximum size " +
- getFormat().MAX_DATABASE_SIZE);
- }
- if((size % getFormat().PAGE_SIZE) != 0L) {
- throw new IOException("Database corrupted, file size " + size +
- " is not multiple of page size " +
- getFormat().PAGE_SIZE);
- }
-
- _forceBytes.rewind();
-
- // push the buffer to the end of the page, so that a full page's worth of
- // data is written
- int pageOffset = (getFormat().PAGE_SIZE - _forceBytes.remaining());
- long offset = size + pageOffset;
- int pageNumber = getNextPageNumber(size);
-
- // since we are just allocating page space at this point and not writing
- // meaningful data, we do _not_ encode the page.
- _channel.write(_forceBytes, offset);
-
- // note, we "force" page removal because we know that this is an unused
- // page (since we just added it to the file)
- _globalUsageMap.removePageNumber(pageNumber, true);
- return pageNumber;
- }
-
- /**
- * Deallocate a previously used page in the database.
- */
- public void deallocatePage(int pageNumber) throws IOException {
- validatePageNumber(pageNumber);
-
- // don't write the whole page, just wipe out the header (which should be
- // enough to let us know if we accidentally try to use an invalid page)
- _invalidPageBytes.rewind();
- _channel.write(_invalidPageBytes, getPageOffset(pageNumber));
-
- _globalUsageMap.addPageNumber(pageNumber); //force is done here
- }
-
- /**
- * @return A newly-allocated buffer that can be passed to readPage
- */
- public ByteBuffer createPageBuffer() {
- return createBuffer(getFormat().PAGE_SIZE);
- }
-
- /**
- * @return A newly-allocated buffer of the given size and DEFAULT_BYTE_ORDER
- * byte order
- */
- public ByteBuffer createBuffer(int size) {
- return createBuffer(size, DEFAULT_BYTE_ORDER);
- }
-
- /**
- * @return A newly-allocated buffer of the given size and byte order
- */
- public ByteBuffer createBuffer(int size, ByteOrder order) {
- return ByteBuffer.allocate(size).order(order);
- }
-
- public void flush() throws IOException {
- _channel.force(true);
- }
-
- public void close() throws IOException {
- flush();
- _channel.close();
- }
-
- public boolean isOpen() {
- return _channel.isOpen();
- }
-
- /**
- * Applies the XOR mask to the database header in the given buffer.
- */
- private void applyHeaderMask(ByteBuffer buffer) {
- // de/re-obfuscate the header
- byte[] headerMask = _format.HEADER_MASK;
- for(int idx = 0; idx < headerMask.length; ++idx) {
- int pos = idx + _format.OFFSET_MASKED_HEADER;
- byte b = (byte)(buffer.get(pos) ^ headerMask[idx]);
- buffer.put(pos, b);
- }
- }
-
- /**
- * @return a duplicate of the current buffer narrowed to the given position
- * and limit. mark will be set at the current position.
- */
- public static ByteBuffer narrowBuffer(ByteBuffer buffer, int position,
- int limit)
- {
- return (ByteBuffer)buffer.duplicate()
- .order(buffer.order())
- .clear()
- .limit(limit)
- .position(position)
- .mark();
- }
- }
|