diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2016-12-31 17:55:42 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2016-12-31 17:55:42 +0000 |
commit | cec8681020e062c056233ef706af78d37d808f91 (patch) | |
tree | bcfe190fdeaa019d51e7286907bb3abd0463a6c1 | |
parent | 9664daf8b3d109ea34964cd0584a903190112c47 (diff) | |
parent | 0114aa89c5a9b9c1c66a1ea4fd70b311b6f0c2b0 (diff) | |
download | jackcess-cec8681020e062c056233ef706af78d37d808f91.tar.gz jackcess-cec8681020e062c056233ef706af78d37d808f91.zip |
merge trunk changes through r1077
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1078 f203690c-595d-4dc9-a70b-905162fa7fd2
15 files changed, 488 insertions, 155 deletions
@@ -3,13 +3,13 @@ <parent> <groupId>com.healthmarketscience</groupId> <artifactId>openhms-parent</artifactId> - <version>1.1.4</version> + <version>1.1.7</version> </parent> <groupId>com.healthmarketscience.jackcess</groupId> <artifactId>jackcess</artifactId> <name>Jackcess</name> <description>A pure Java library for reading from and writing to MS Access databases.</description> - <version>2.1.6-SNAPSHOT</version> + <version>2.1.7-SNAPSHOT</version> <url>http://jackcess.sf.net</url> <inceptionYear>2005</inceptionYear> <developers> @@ -118,7 +118,6 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> - <version>2.15</version> <configuration> <forkMode>once</forkMode> <parallel>classes</parallel> @@ -267,13 +266,6 @@ <SourceForge2Patches>http://sourceforge.net/p/jackcess/patches/%ISSUE%</SourceForge2Patches> </issueLinkTemplatePerSystem> </configuration> - <reportSets> - <reportSet> - <reports> - <report>changes-report</report> - </reports> - </reportSet> - </reportSets> </plugin> <plugin> <artifactId>maven-javadoc-plugin</artifactId> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 73cf393..2191cd4 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,17 @@ <author email="javajedi@users.sf.net">Tim McCune</author> </properties> <body> + <release version="2.1.6" date="2016-11-29"> + <action dev="jahlborn" type="update" system="SourceForge2Features" + issue="34"> + When opening a database for a format which is read-only, automatically + open the channel as read-only (instead of throwing an exception if + readOnly is false). + </action> + <action dev="jahlborn" type="fix" system="SourceForge2" issue="138"> + Add support for global usage maps which are reference type maps. + </action> + </release> <release version="2.1.5" date="2016-10-03"> <action dev="jahlborn" type="update"> Change multi-value complex columns so that they return all relevant diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 68dff16..319879b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -70,6 +70,7 @@ import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; import com.healthmarketscience.jackcess.util.ColumnValidatorFactory; import com.healthmarketscience.jackcess.util.ErrorHandler; import com.healthmarketscience.jackcess.util.LinkResolver; +import com.healthmarketscience.jackcess.util.ReadOnlyFileChannel; import com.healthmarketscience.jackcess.util.SimpleColumnValidatorFactory; import com.healthmarketscience.jackcess.util.TableIterableBuilder; import org.apache.commons.lang.builder.ToStringBuilder; @@ -390,13 +391,14 @@ public class DatabaseImpl implements Database JetFormat jetFormat = JetFormat.getFormat(channel); if(jetFormat.READ_ONLY) { - throw new IOException("file format " + - jetFormat.getPossibleFileFormats().values() + - " does not support writing for " + mdbFile); + // wrap the channel with a read-only version to enforce + // non-writability + channel = new ReadOnlyFileChannel(channel); + readOnly = true; } } - DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, + DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, null, charset, timeZone, provider); success = true; return db; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java index a55259e..ba8be57 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -465,7 +465,7 @@ public class IndexData { } } - _ownedPages = UsageMap.read(getTable().getDatabase(), tableBuffer, false); + _ownedPages = UsageMap.read(getTable().getDatabase(), tableBuffer); _rootPageNumber = tableBuffer.getInt(); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java index c60daf5..4ee1882 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java @@ -994,7 +994,7 @@ public class IndexPageCache */ private void validateEntries(DataPageExtra dpExtra) throws IOException { int entrySize = 0; - Entry prevEntry = IndexData.FIRST_ENTRY; + Entry prevEntry = FIRST_ENTRY; for(Entry e : dpExtra._entries) { entrySize += e.size(); if(prevEntry.compareTo(e) >= 0) { @@ -1333,8 +1333,7 @@ public class IndexPageCache /** * IndexPageCache implementation of an Index {@link DataPage}. */ - private static final class CacheDataPage - extends IndexData.DataPage + private static final class CacheDataPage extends DataPage { public final DataPageMain _main; public final DataPageExtra _extra; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java index 8a137ea..73648b3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java @@ -523,7 +523,7 @@ class LongValueColumnImpl extends ColumnImpl public void clear() throws IOException { int pageNumber = getPageNumber(); if(pageNumber != PageChannel.INVALID_PAGE_NUMBER) { - _freeSpacePages.removePageNumber(pageNumber, true); + _freeSpacePages.removePageNumber(pageNumber); } super.clear(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java b/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java index 00dabbe..19d15fd 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java @@ -347,9 +347,7 @@ public class PageChannel implements Channel, Flushable { // 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); + _globalUsageMap.removePageNumber(pageNumber); return pageNumber; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java index aee71ae..2b41fe7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java @@ -330,7 +330,7 @@ public class RelationshipCreator extends DBMutator } return CustomToStringStyle.valueBuilder(tableName) - .append(null, cols) + .append(null, colNames) .toString(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index e5a6316..5155b16 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -244,9 +244,9 @@ public class TableImpl implements Table _indexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEXES); tableBuffer.position(getFormat().OFFSET_OWNED_PAGES); - _ownedPages = UsageMap.read(getDatabase(), tableBuffer, false); + _ownedPages = UsageMap.read(getDatabase(), tableBuffer); tableBuffer.position(getFormat().OFFSET_FREE_SPACE_PAGES); - _freeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false); + _freeSpacePages = UsageMap.read(getDatabase(), tableBuffer); for (int i = 0; i < _indexCount; i++) { _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat())); @@ -1948,8 +1948,8 @@ public class TableImpl implements Table UsageMap colOwnedPages = null; UsageMap colFreeSpacePages = null; try { - colOwnedPages = UsageMap.read(getDatabase(), tableBuffer, false); - colFreeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false); + colOwnedPages = UsageMap.read(getDatabase(), tableBuffer); + colFreeSpacePages = UsageMap.read(getDatabase(), tableBuffer); } catch(IllegalStateException e) { // ignore invalid usage map info colOwnedPages = null; @@ -2547,7 +2547,7 @@ public class TableImpl implements Table if(modifiedPage) { writeDataPage(dataPage, pageNumber); } - _freeSpacePages.removePageNumber(pageNumber, true); + _freeSpacePages.removePageNumber(pageNumber); dataPage = newDataPage(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/UsageMap.java b/src/main/java/com/healthmarketscience/jackcess/impl/UsageMap.java index 898031f..c67bcba 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/UsageMap.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/UsageMap.java @@ -92,15 +92,14 @@ public class UsageMap public PageChannel getPageChannel() { return getDatabase().getPageChannel(); } - + /** * @param database database that contains this usage map * @param buf buffer which contains the usage map row info * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on * which type of map is found */ - public static UsageMap read(DatabaseImpl database, ByteBuffer buf, - boolean assumeOutOfRangeBitsOn) + public static UsageMap read(DatabaseImpl database, ByteBuffer buf) throws IOException { int umapRowNum = buf.get(); @@ -112,11 +111,12 @@ public class UsageMap * @param database database that contains this usage map * @param pageNum Page number that this usage map is contained in * @param rowNum Number of the row on the page that contains this usage map + * @param isGlobal whether or not we are reading the "global" usage map * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on * which type of map is found */ - public static UsageMap read(DatabaseImpl database, int pageNum, - int rowNum, boolean assumeOutOfRangeBitsOn) + static UsageMap read(DatabaseImpl database, int pageNum, + int rowNum, boolean isGlobal) throws IOException { if(pageNum <= 0) { @@ -133,17 +133,19 @@ public class UsageMap tableBuffer.limit(rowEnd); byte mapType = tableBuffer.get(rowStart); UsageMap rtn = new UsageMap(database, tableBuffer, pageNum, rowStart); - rtn.initHandler(mapType, assumeOutOfRangeBitsOn); + rtn.initHandler(mapType, isGlobal); return rtn; } - private void initHandler(byte mapType, boolean assumeOutOfRangeBitsOn) + private void initHandler(byte mapType, boolean isGlobal) throws IOException { if (mapType == MAP_TYPE_INLINE) { - _handler = new InlineHandler(assumeOutOfRangeBitsOn); + _handler = (isGlobal ? new GlobalInlineHandler() : + new InlineHandler()); } else if (mapType == MAP_TYPE_REFERENCE) { - _handler = new ReferenceHandler(); + _handler = (isGlobal ? new GlobalReferenceHandler() : + new ReferenceHandler()); } else { throw new IOException(MSG_PREFIX_UNRECOGNIZED_MAP + mapType); } @@ -315,7 +317,13 @@ public class UsageMap /** * Remove a page number from this usage map */ - protected void removePageNumber(int pageNumber, boolean force) + public void removePageNumber(int pageNumber) + throws IOException + { + removePageNumber(pageNumber, true); + } + + private void removePageNumber(int pageNumber, boolean force) throws IOException { ++_modCount; @@ -467,28 +475,25 @@ public class UsageMap */ private class InlineHandler extends Handler { - private final boolean _assumeOutOfRangeBitsOn; private final int _maxInlinePages; - private InlineHandler(boolean assumeOutOfRangeBitsOn) - throws IOException + protected InlineHandler() throws IOException { - _assumeOutOfRangeBitsOn = assumeOutOfRangeBitsOn; _maxInlinePages = (getInlineDataEnd() - getInlineDataStart()) * 8; int startPage = getTableBuffer().getInt(getRowStart() + 1); setInlinePageRange(startPage); processMap(getTableBuffer(), 0); } - private int getMaxInlinePages() { + protected final int getMaxInlinePages() { return _maxInlinePages; } - private int getInlineDataStart() { + protected final int getInlineDataStart() { return getRowStart() + getFormat().OFFSET_USAGE_MAP_START; } - private int getInlineDataEnd() { + protected final int getInlineDataEnd() { return getRowEnd(); } @@ -499,13 +504,6 @@ public class UsageMap private void setInlinePageRange(int startPage) { setPageRange(startPage, startPage + getMaxInlinePages()); } - - @Override - public boolean containsPageNumber(int pageNumber) { - return(super.containsPageNumber(pageNumber) || - (_assumeOutOfRangeBitsOn && (pageNumber >= 0) && - !isPageWithinRange(pageNumber))); - } @Override public void addOrRemovePageNumber(int pageNumber, boolean add, @@ -523,68 +521,55 @@ public class UsageMap } else { - // uh-oh, we've split our britches. what now? determine what our - // status is + // uh-oh, we've split our britches. what now? + addOrRemovePageNumberOutsideRange(pageNumber, add, force); + } + } + + protected void addOrRemovePageNumberOutsideRange( + int pageNumber, boolean add, boolean force) + throws IOException + { + // determine what our status is before taking action + + if(add) { + int firstPage = getFirstPageNumber(); int lastPage = getLastPageNumber(); - - if(add) { - // we can ignore out-of-range page addition if we are already - // assuming out-of-range bits are "on". Note, we are leaving small - // holes in the database here (leaving behind some free pages), but - // it's not the end of the world. - 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 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); + // 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 { + // not going to happen, need to promote the usage map to a + // reference map + promoteInlineHandlerToReferenceHandler(pageNumber); + } - // we are removing, what does that mean? - if(_assumeOutOfRangeBitsOn) { + } else { - // we are using an inline map and assuming that anything not - // within the current range is "on". so, if we attempt to set a - // bit which is before the current page, ignore it, we are not - // going back for it. - if((firstPage <= PageChannel.INVALID_PAGE_NUMBER) || - (pageNumber > lastPage)) { + // we are removing, what does that mean? + if(!force) { - // move to new start page, filling in as we move - moveToNewStartPageForRemove(firstPage, pageNumber); - - } - - } else if(!force) { - - // 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" + - ", expected range " + - _startPage + " to " + _endPage); - } + // 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" + + ", expected range " + + _startPage + " to " + _endPage); } - } } @@ -594,7 +579,7 @@ public class UsageMap * @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) + protected final void moveToNewStartPage(int newStartPage, int newPageNumber) throws IOException { int oldStartPage = getStartPage(); @@ -617,6 +602,57 @@ public class UsageMap // put the pages back in reAddPages(oldStartPage, oldPageNumbers, newPageNumber); } + } + + /** + * Modified version of an "inline" usage map used for the global usage map. + * When an inline usage map is used for the global usage map, we assume + * out-of-range bits are on. We never promote the global usage map to a + * reference usage map (although ms access may). + * + * Note, this UsageMap does not implement all the methods "correctly". Only + * addPageNumber and removePageNumber should be called by PageChannel. + */ + private class GlobalInlineHandler extends InlineHandler + { + private GlobalInlineHandler() throws IOException { + } + + @Override + public boolean containsPageNumber(int pageNumber) { + // should never be called on global map + throw new UnsupportedOperationException(); + } + + @Override + protected void addOrRemovePageNumberOutsideRange( + int pageNumber, boolean add, boolean force) + throws IOException + { + // determine what our status is + + // for the global usage map, we can ignore out-of-range page addition + // since we assuming out-of-range bits are "on". Note, we are leaving + // small holes in the database here (leaving behind some free pages), + // but it's not the end of the world. + + if(!add) { + + int firstPage = getFirstPageNumber(); + int lastPage = getLastPageNumber(); + + // we are using an inline map and assuming that anything not + // within the current range is "on". so, if we attempt to set a + // bit which is before the current page, ignore it, we are not + // going back for it. + if((firstPage <= PageChannel.INVALID_PAGE_NUMBER) || + (pageNumber > lastPage)) { + + // move to new start page, filling in as we move + moveToNewStartPageForRemove(firstPage, pageNumber); + } + } + } /** * Shifts the inline usage map so that it now starts with the given @@ -675,13 +711,15 @@ public class UsageMap /** Buffer that contains the current reference map page */ private final TempPageHolder _mapPageHolder = TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + private final int _maxPagesPerUsageMapPage; - private ReferenceHandler() - throws IOException + private ReferenceHandler() throws IOException { + _maxPagesPerUsageMapPage = ((getFormat().PAGE_SIZE - + getFormat().OFFSET_USAGE_MAP_PAGE_DATA) * 8); int numUsagePages = (getRowEnd() - getRowStart() - 1) / 4; setStartOffset(getFormat().OFFSET_USAGE_MAP_PAGE_DATA); - setPageRange(0, (numUsagePages * getMaxPagesPerUsagePage())); + setPageRange(0, (numUsagePages * _maxPagesPerUsageMapPage)); // 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 @@ -699,14 +737,13 @@ public class UsageMap pageType); } mapPageBuffer.position(getFormat().OFFSET_USAGE_MAP_PAGE_DATA); - processMap(mapPageBuffer, (getMaxPagesPerUsagePage() * i)); + processMap(mapPageBuffer, (_maxPagesPerUsageMapPage * i)); } } } - private int getMaxPagesPerUsagePage() { - return((getFormat().PAGE_SIZE - getFormat().OFFSET_USAGE_MAP_PAGE_DATA) - * 8); + protected final int getMaxPagesPerUsagePage() { + return _maxPagesPerUsageMapPage; } @Override @@ -745,10 +782,7 @@ public class UsageMap */ private ByteBuffer createNewUsageMapPage(int pageIndex) throws IOException { - ByteBuffer mapPageBuffer = _mapPageHolder.setNewPage(getPageChannel()); - mapPageBuffer.put(PageTypes.USAGE_MAP); - mapPageBuffer.put((byte) 0x01); //Unknown - mapPageBuffer.putShort((short) 0); //Unknown + ByteBuffer mapPageBuffer = allocateNewUsageMapPage(pageIndex); int mapPageNum = _mapPageHolder.getPageNumber(); getTableBuffer().putInt(calculateMapPagePointerOffset(pageIndex), mapPageNum); @@ -760,6 +794,108 @@ public class UsageMap return getRowStart() + getFormat().OFFSET_REFERENCE_MAP_PAGE_NUMBERS + (pageIndex * 4); } + + protected ByteBuffer allocateNewUsageMapPage(int pageIndex) + throws IOException + { + ByteBuffer mapPageBuffer = _mapPageHolder.setNewPage(getPageChannel()); + mapPageBuffer.put(PageTypes.USAGE_MAP); + mapPageBuffer.put((byte) 0x01); //Unknown + mapPageBuffer.putShort((short) 0); //Unknown + return mapPageBuffer; + } + } + + /** + * Modified version of a "reference" usage map used for the global usage + * map. Since reference usage maps require allocating pages for their own + * use, we need to handle potential cycles where the PageChannel is + * attempting to allocate a new page (and remove it from the global usage + * map) and this usage map also needs to allocate a new page. When that + * happens, we stash the pending information from the PageChannel and handle + * it after we have retrieved the new page. + * + * Note, this UsageMap does not implement all the methods "correctly". Only + * addPageNumber and removePageNumber should be called by PageChannel. + */ + private class GlobalReferenceHandler extends ReferenceHandler + { + private boolean _allocatingPage; + private Integer _pendingPage; + + private GlobalReferenceHandler() throws IOException { + } + + @Override + public boolean containsPageNumber(int pageNumber) { + // should never be called on global map + throw new UnsupportedOperationException(); + } + + @Override + public void addOrRemovePageNumber(int pageNumber, boolean add, + boolean force) + throws IOException + { + if(_allocatingPage && !add) { + // we are in the midst of allocating a page for ourself, keep track of + // this new page so we can mark it later... + if(_pendingPage != null) { + throw new IllegalStateException("should only have single pending page"); + } + _pendingPage = pageNumber; + return; + } + + super.addOrRemovePageNumber(pageNumber, add, force); + + while(_pendingPage != null) { + + // while updating our usage map, we needed to allocate a new page (and + // thus mark a new page as used). we delayed that marking so that we + // didn't get into an infinite loop. now that we completed the + // original updated, handle the new page. (we use a loop under the + // off the wall chance that adding this page requires allocating a new + // page. in theory, we could do this more than once, but not + // forever). + int removedPageNumber = _pendingPage; + _pendingPage = null; + + super.addOrRemovePageNumber(removedPageNumber, false, true); + } + } + + @Override + protected ByteBuffer allocateNewUsageMapPage(int pageIndex) + throws IOException + { + try { + // keep track of the fact that we are actively allocating a page for our + // own use so that we can break the potential cycle. + _allocatingPage = true; + + ByteBuffer mapPageBuffer = super.allocateNewUsageMapPage(pageIndex); + + // for the global usage map, all pages are "on" by default. so + // whenever we add a new backing page to the usage map, we need to + // turn all the pages that it represents to "on" (we essentially lazy + // load this map, which is fine because we should only add pages which + // represent the size of the database currently in use). + int dataStart = getFormat().OFFSET_USAGE_MAP_PAGE_DATA; + ByteUtil.fillRange(mapPageBuffer, dataStart, + getFormat().PAGE_SIZE - dataStart); + + int maxPagesPerUmapPage = getMaxPagesPerUsagePage(); + int firstNewPage = (pageIndex * maxPagesPerUmapPage); + int lastNewPage = firstNewPage + maxPagesPerUmapPage; + _pageNumbers.set(firstNewPage, lastNewPage); + + return mapPageBuffer; + + } finally { + _allocatingPage = false; + } + } } /** diff --git a/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java b/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java new file mode 100644 index 0000000..85620e8 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java @@ -0,0 +1,144 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.util; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import com.healthmarketscience.jackcess.Database; + +/** + * Wrapper for existing FileChannel which is read-only. + * <p/> + * Implementation note: this class is optimized for use with {@link Database}. + * Therefore not all methods may be implemented. + * + * @author James Ahlborn + * @usage _advanced_class_ + */ +public class ReadOnlyFileChannel extends FileChannel +{ + private final FileChannel _delegate; + + public ReadOnlyFileChannel(FileChannel delegate) { + _delegate = delegate; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + return _delegate.read(dst); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + return _delegate.read(dsts, offset, length); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException { + return _delegate.read(dst, position); + } + + @Override + public long position() throws IOException { + return _delegate.position(); + } + + @Override + public FileChannel position(long newPosition) throws IOException { + _delegate.position(newPosition); + return this; + } + + @Override + public long size() throws IOException { + return _delegate.size(); + } + + @Override + public FileChannel truncate(long size) throws IOException { + throw new NonWritableChannelException(); + } + + @Override + public void force(boolean metaData) throws IOException { + // do nothing + } + + @Override + public long transferTo(long position, long count, WritableByteChannel target) + throws IOException + { + return _delegate.transferTo(position, count, target); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) + throws IOException + { + throw new NonWritableChannelException(); + } + + @Override + public int write(ByteBuffer src, long position) throws IOException { + throw new NonWritableChannelException(); + } + + + @Override + public int write(ByteBuffer src) throws IOException { + throw new NonWritableChannelException(); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) + throws IOException + { + throw new NonWritableChannelException(); + } + + @Override + public MappedByteBuffer map(MapMode mode, long position, long size) + throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public FileLock lock(long position, long size, boolean shared) + throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) + throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + protected void implCloseChannel() throws IOException { + _delegate.close(); + } +} diff --git a/src/site/site.xml b/src/site/site.xml index cbeaf24..f79e3a6 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -4,7 +4,7 @@ <skin> <groupId>org.apache.maven.skins</groupId> <artifactId>maven-fluido-skin</artifactId> - <version>1.4</version> + <version>1.6</version> </skin> <bannerLeft> <name>${project.name}</name> diff --git a/src/test/data/V2000/testRefGlobalV2000.mdb b/src/test/data/V2000/testRefGlobalV2000.mdb Binary files differnew file mode 100644 index 0000000..7ebd402 --- /dev/null +++ b/src/test/data/V2000/testRefGlobalV2000.mdb diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java index 93c5ca9..4efbd60 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java @@ -1,8 +1,8 @@ package com.healthmarketscience.jackcess.impl; import java.io.File; -import java.io.IOException; import java.nio.channels.FileChannel; +import java.nio.channels.NonWritableChannelException; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -11,6 +11,7 @@ import java.util.Set; import com.healthmarketscience.jackcess.Database; import static com.healthmarketscience.jackcess.Database.*; import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.PropertyMap; import junit.framework.TestCase; import static com.healthmarketscience.jackcess.TestUtil.*; @@ -208,10 +209,17 @@ public class JetFormatTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { Database db = null; - IOException failure = null; + Exception failure = null; try { db = openCopy(testDB); - } catch(IOException e) { + + if(testDB.getExpectedFormat().READ_ONLY) { + PropertyMap props = db.getUserDefinedProperties(); + props.put("foo", "bar"); + props.save(); + } + + } catch(Exception e) { failure = e; } finally { if(db != null) { @@ -222,7 +230,7 @@ public class JetFormatTest extends TestCase { if(!testDB.getExpectedFormat().READ_ONLY) { assertNull(failure); } else { - assertTrue(failure.getMessage().contains("does not support writing")); + assertTrue(failure instanceof NonWritableChannelException); } } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java index aad1ddf..539a7c0 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/UsageMapTest.java @@ -2,11 +2,18 @@ package com.healthmarketscience.jackcess.impl; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseBuilder; -import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; import junit.framework.TestCase; +import static com.healthmarketscience.jackcess.TestUtil.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; /** * @author Dan Rollo @@ -15,40 +22,76 @@ import junit.framework.TestCase; */ public final class UsageMapTest extends TestCase { - public void testRead() throws Exception { - for (final TestDB testDB : SUPPORTED_DBS_TEST) { - final int expectedFirstPage; - final int expectedLastPage; - final Database.FileFormat expectedFileFormat = testDB.getExpectedFileFormat(); - if (Database.FileFormat.V2000.equals(expectedFileFormat)) { - expectedFirstPage = 743; - expectedLastPage = 767; - } else if (Database.FileFormat.V2003.equals(expectedFileFormat)) { - expectedFirstPage = 16; - expectedLastPage = 799; - } else if (Database.FileFormat.V2007.equals(expectedFileFormat)) { - expectedFirstPage = 94; - expectedLastPage = 511; - } else if (Database.FileFormat.V2010.equals(expectedFileFormat)) { - expectedFirstPage = 109; - expectedLastPage = 511; - } else { - throw new IllegalAccessException("Unknown file format: " + expectedFileFormat); - } - checkUsageMapRead(testDB.getFile(), expectedFirstPage, expectedLastPage); - } + public void testRead() throws Exception { + for (final TestDB testDB : SUPPORTED_DBS_TEST) { + final int expectedFirstPage; + final int expectedLastPage; + final Database.FileFormat expectedFileFormat = testDB.getExpectedFileFormat(); + if (Database.FileFormat.V2000.equals(expectedFileFormat)) { + expectedFirstPage = 743; + expectedLastPage = 767; + } else if (Database.FileFormat.V2003.equals(expectedFileFormat)) { + expectedFirstPage = 16; + expectedLastPage = 799; + } else if (Database.FileFormat.V2007.equals(expectedFileFormat)) { + expectedFirstPage = 94; + expectedLastPage = 511; + } else if (Database.FileFormat.V2010.equals(expectedFileFormat)) { + expectedFirstPage = 109; + expectedLastPage = 511; + } else { + throw new IllegalAccessException("Unknown file format: " + expectedFileFormat); + } + checkUsageMapRead(testDB.getFile(), expectedFirstPage, expectedLastPage); } + } + + private static void checkUsageMapRead( + final File dbFile, final int expectedFirstPage, final int expectedLastPage) + throws IOException { + + final Database db = DatabaseBuilder.open(dbFile); + final UsageMap usageMap = UsageMap.read((DatabaseImpl)db, + PageChannel.PAGE_GLOBAL_USAGE_MAP, + PageChannel.ROW_GLOBAL_USAGE_MAP, + true); + assertEquals("Unexpected FirstPageNumber.", expectedFirstPage, + usageMap.getFirstPageNumber()); + assertEquals("Unexpected LastPageNumber.", expectedLastPage, + usageMap.getLastPageNumber()); + } + + public void testGobalReferenceUsageMap() throws Exception + { + Database db = openCopy( + Database.FileFormat.V2000, + new File("src/test/data/V2000/testRefGlobalV2000.mdb")); - private static void checkUsageMapRead(final File dbFile, - final int expectedFirstPage, final int expectedLastPage) - throws IOException { - - final Database db = DatabaseBuilder.open(dbFile); - final UsageMap usageMap = UsageMap.read((DatabaseImpl)db, - PageChannel.PAGE_GLOBAL_USAGE_MAP, - PageChannel.ROW_GLOBAL_USAGE_MAP, - true); - assertEquals("Unexpected FirstPageNumber.", expectedFirstPage, usageMap.getFirstPageNumber()); - assertEquals("Unexpected LastPageNumber.", expectedLastPage, usageMap.getLastPageNumber()); + Table t = new TableBuilder("Test2") + .addColumn(new ColumnBuilder("id", DataType.LONG)) + .addColumn(new ColumnBuilder("data1", DataType.TEXT)) + .addColumn(new ColumnBuilder("data2", DataType.TEXT)) + .toTable(db); + + + ((DatabaseImpl)db).getPageChannel().startWrite(); + try { + List<Object[]> rows = new ArrayList<Object[]>(); + for(int i = 0; i < 300000; ++i) { + String s1 = "r" + i + "-" + createString(100); + String s2 = "r" + i + "-" + createString(200); + + rows.add(new Object[]{i, s1, s2}); + + if((i % 2000) == 0) { + t.addRows(rows); + rows.clear(); + } + } + } finally { + ((DatabaseImpl)db).getPageChannel().finishWrite(); } + + db.close(); + } } |