001: /*
002: Copyright (c) 2005 Health Market Science, Inc.
003:
004: This library is free software; you can redistribute it and/or
005: modify it under the terms of the GNU Lesser General Public
006: License as published by the Free Software Foundation; either
007: version 2.1 of the License, or (at your option) any later version.
008:
009: This library is distributed in the hope that it will be useful,
010: but WITHOUT ANY WARRANTY; without even the implied warranty of
011: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: Lesser General Public License for more details.
013:
014: You should have received a copy of the GNU Lesser General Public
015: License along with this library; if not, write to the Free Software
016: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
017: USA
018:
019: You can contact Health Market Science at info@healthmarketscience.com
020: or at the following address:
021:
022: Health Market Science
023: 2700 Horizon Drive
024: Suite 200
025: King of Prussia, PA 19406
026: */
027:
028: package com.healthmarketscience.jackcess;
029:
030: import java.io.IOException;
031: import java.nio.ByteBuffer;
032: import java.util.BitSet;
033:
034: import org.apache.commons.logging.Log;
035: import org.apache.commons.logging.LogFactory;
036:
037: /**
038: * Describes which database pages a particular table uses
039: * @author Tim McCune
040: */
041: public class UsageMap {
042: private static final Log LOG = LogFactory.getLog(UsageMap.class);
043:
044: /** Inline map type */
045: public static final byte MAP_TYPE_INLINE = 0x0;
046: /** Reference map type, for maps that are too large to fit inline */
047: public static final byte MAP_TYPE_REFERENCE = 0x1;
048:
049: /** bit index value for an invalid page number */
050: private static final int INVALID_BIT_INDEX = -1;
051:
052: /** owning database */
053: private final Database _database;
054: /** Page number of the map table declaration */
055: private final int _tablePageNum;
056: /** Offset of the data page at which the usage map data starts */
057: private int _startOffset;
058: /** Offset of the data page at which the usage map declaration starts */
059: private final short _rowStart;
060: /** First page that this usage map applies to */
061: private int _startPage;
062: /** Last page that this usage map applies to */
063: private int _endPage;
064: /** bits representing page numbers used, offset from _startPage */
065: private BitSet _pageNumbers = new BitSet();
066: /** Buffer that contains the usage map table declaration page */
067: private final ByteBuffer _tableBuffer;
068: /** modification count on the usage map, used to keep the cursors in
069: sync */
070: private int _modCount;
071: /** the current handler implementation for reading/writing the specific
072: usage map type. note, this may change over time. */
073: private Handler _handler;
074:
075: /**
076: * @param database database that contains this usage map
077: * @param tableBuffer Buffer that contains this map's declaration
078: * @param pageNum Page number that this usage map is contained in
079: * @param rowStart Offset at which the declaration starts in the buffer
080: */
081: private UsageMap(Database database, ByteBuffer tableBuffer,
082: int pageNum, short rowStart) throws IOException {
083: _database = database;
084: _tableBuffer = tableBuffer;
085: _tablePageNum = pageNum;
086: _rowStart = rowStart;
087: _tableBuffer.position(_rowStart
088: + getFormat().OFFSET_USAGE_MAP_START);
089: _startOffset = _tableBuffer.position();
090: if (LOG.isDebugEnabled()) {
091: LOG.debug("Usage map block:\n"
092: + ByteUtil.toHexString(_tableBuffer, _rowStart,
093: tableBuffer.limit() - _rowStart));
094: }
095: }
096:
097: public Database getDatabase() {
098: return _database;
099: }
100:
101: public JetFormat getFormat() {
102: return getDatabase().getFormat();
103: }
104:
105: public PageChannel getPageChannel() {
106: return getDatabase().getPageChannel();
107: }
108:
109: /**
110: * @param database database that contains this usage map
111: * @param pageNum Page number that this usage map is contained in
112: * @param rowNum Number of the row on the page that contains this usage map
113: * @return Either an InlineUsageMap or a ReferenceUsageMap, depending on
114: * which type of map is found
115: */
116: public static UsageMap read(Database database, int pageNum,
117: int rowNum, boolean assumeOutOfRangeBitsOn)
118: throws IOException {
119: JetFormat format = database.getFormat();
120: PageChannel pageChannel = database.getPageChannel();
121: ByteBuffer tableBuffer = pageChannel.createPageBuffer();
122: pageChannel.readPage(tableBuffer, pageNum);
123: short rowStart = Table
124: .findRowStart(tableBuffer, rowNum, format);
125: int rowEnd = Table.findRowEnd(tableBuffer, rowNum, format);
126: tableBuffer.limit(rowEnd);
127: byte mapType = tableBuffer.get(rowStart);
128: UsageMap rtn = new UsageMap(database, tableBuffer, pageNum,
129: rowStart);
130: rtn.initHandler(mapType, assumeOutOfRangeBitsOn);
131: return rtn;
132: }
133:
134: private void initHandler(byte mapType,
135: boolean assumeOutOfRangeBitsOn) throws IOException {
136: if (mapType == MAP_TYPE_INLINE) {
137: _handler = new InlineHandler(assumeOutOfRangeBitsOn);
138: } else if (mapType == MAP_TYPE_REFERENCE) {
139: _handler = new ReferenceHandler();
140: } else {
141: throw new IOException("Unrecognized map type: " + mapType);
142: }
143: }
144:
145: public PageCursor cursor() {
146: return new PageCursor();
147: }
148:
149: protected short getRowStart() {
150: return _rowStart;
151: }
152:
153: protected int getRowEnd() {
154: return getTableBuffer().limit();
155: }
156:
157: protected void setStartOffset(int startOffset) {
158: _startOffset = startOffset;
159: }
160:
161: protected int getStartOffset() {
162: return _startOffset;
163: }
164:
165: protected ByteBuffer getTableBuffer() {
166: return _tableBuffer;
167: }
168:
169: protected int getTablePageNumber() {
170: return _tablePageNum;
171: }
172:
173: protected int getStartPage() {
174: return _startPage;
175: }
176:
177: protected int getEndPage() {
178: return _endPage;
179: }
180:
181: protected BitSet getPageNumbers() {
182: return _pageNumbers;
183: }
184:
185: protected void setPageRange(int newStartPage, int newEndPage) {
186: _startPage = newStartPage;
187: _endPage = newEndPage;
188: }
189:
190: protected boolean isPageWithinRange(int pageNumber) {
191: return ((pageNumber >= _startPage) && (pageNumber < _endPage));
192: }
193:
194: protected int getFirstPageNumber() {
195: return bitIndexToPageNumber(getNextBitIndex(-1),
196: RowId.LAST_PAGE_NUMBER);
197: }
198:
199: protected int getNextPageNumber(int curPage) {
200: return bitIndexToPageNumber(
201: getNextBitIndex(pageNumberToBitIndex(curPage)),
202: RowId.LAST_PAGE_NUMBER);
203: }
204:
205: protected int getNextBitIndex(int curIndex) {
206: return _pageNumbers.nextSetBit(curIndex + 1);
207: }
208:
209: protected int getLastPageNumber() {
210: return bitIndexToPageNumber(getPrevBitIndex(_pageNumbers
211: .length()), RowId.FIRST_PAGE_NUMBER);
212: }
213:
214: protected int getPrevPageNumber(int curPage) {
215: return bitIndexToPageNumber(
216: getPrevBitIndex(pageNumberToBitIndex(curPage)),
217: RowId.FIRST_PAGE_NUMBER);
218: }
219:
220: protected int getPrevBitIndex(int curIndex) {
221: --curIndex;
222: while ((curIndex >= 0) && !_pageNumbers.get(curIndex)) {
223: --curIndex;
224: }
225: return curIndex;
226: }
227:
228: protected int bitIndexToPageNumber(int bitIndex,
229: int invalidPageNumber) {
230: return ((bitIndex >= 0) ? (_startPage + bitIndex)
231: : invalidPageNumber);
232: }
233:
234: protected int pageNumberToBitIndex(int pageNumber) {
235: return ((pageNumber >= 0) ? (pageNumber - _startPage)
236: : INVALID_BIT_INDEX);
237: }
238:
239: protected void clearTableAndPages() {
240: // reset some values
241: _pageNumbers.clear();
242: _startPage = 0;
243: _endPage = 0;
244: ++_modCount;
245:
246: // clear out the table data (everything except map type)
247: int tableStart = getRowStart() + 1;
248: int tableEnd = getRowEnd();
249: ByteUtil.clearRange(_tableBuffer, tableStart, tableEnd);
250: }
251:
252: protected void writeTable() throws IOException {
253: // note, we only want to write the row data with which we are working
254: getPageChannel().writePage(_tableBuffer, _tablePageNum,
255: _rowStart);
256: }
257:
258: /**
259: * Read in the page numbers in this inline map
260: */
261: protected void processMap(ByteBuffer buffer, int bufferStartPage) {
262: int byteCount = 0;
263: while (buffer.hasRemaining()) {
264: byte b = buffer.get();
265: if (b != (byte) 0) {
266: for (int i = 0; i < 8; i++) {
267: if ((b & (1 << i)) != 0) {
268: int pageNumberOffset = (byteCount * 8 + i)
269: + bufferStartPage;
270: int pageNumber = bitIndexToPageNumber(
271: pageNumberOffset,
272: PageChannel.INVALID_PAGE_NUMBER);
273: if (!isPageWithinRange(pageNumber)) {
274: throw new IllegalStateException(
275: "found page number "
276: + pageNumber
277: + " in usage map outside of expected range "
278: + _startPage + " to "
279: + _endPage);
280: }
281: _pageNumbers.set(pageNumberOffset);
282: }
283: }
284: }
285: byteCount++;
286: }
287: }
288:
289: /**
290: * Determines if the given page number is contained in this map.
291: */
292: public boolean containsPageNumber(int pageNumber) {
293: return _handler.containsPageNumber(pageNumber);
294: }
295:
296: /**
297: * Add a page number to this usage map
298: */
299: public void addPageNumber(int pageNumber) throws IOException {
300: ++_modCount;
301: _handler.addOrRemovePageNumber(pageNumber, true);
302: }
303:
304: /**
305: * Remove a page number from this usage map
306: */
307: public void removePageNumber(int pageNumber) throws IOException {
308: ++_modCount;
309: _handler.addOrRemovePageNumber(pageNumber, false);
310: }
311:
312: protected void updateMap(int absolutePageNumber,
313: int bufferRelativePageNumber, ByteBuffer buffer, boolean add)
314: throws IOException {
315: //Find the byte to which to apply the bitmask and create the bitmask
316: int offset = bufferRelativePageNumber / 8;
317: int bitmask = 1 << (bufferRelativePageNumber % 8);
318: byte b = buffer.get(_startOffset + offset);
319:
320: // check current value for this page number
321: int pageNumberOffset = pageNumberToBitIndex(absolutePageNumber);
322: boolean isOn = _pageNumbers.get(pageNumberOffset);
323: if (isOn == add) {
324: throw new IOException("Page number " + absolutePageNumber
325: + " already "
326: + ((add) ? "added to" : "removed from")
327: + " usage map, expected range " + _startPage
328: + " to " + _endPage);
329: }
330:
331: //Apply the bitmask
332: if (add) {
333: b |= bitmask;
334: _pageNumbers.set(pageNumberOffset);
335: } else {
336: b &= ~bitmask;
337: _pageNumbers.clear(pageNumberOffset);
338: }
339: buffer.put(_startOffset + offset, b);
340: }
341:
342: /**
343: * Promotes and inline usage map to a reference usage map.
344: */
345: private void promoteInlineHandlerToReferenceHandler(
346: int newPageNumber) throws IOException {
347: // copy current page number info to new references and then clear old
348: int oldStartPage = _startPage;
349: BitSet oldPageNumbers = (BitSet) _pageNumbers.clone();
350:
351: // clear out the main table (inline usage map data and start page)
352: clearTableAndPages();
353:
354: // set the new map type
355: _tableBuffer.put(getRowStart(), MAP_TYPE_REFERENCE);
356:
357: // write the new table data
358: writeTable();
359:
360: // set new handler
361: _handler = new ReferenceHandler();
362:
363: // update new handler with old data
364: reAddPages(oldStartPage, oldPageNumbers, newPageNumber);
365: }
366:
367: private void reAddPages(int oldStartPage, BitSet oldPageNumbers,
368: int newPageNumber) throws IOException {
369: // add all the old pages back in
370: for (int i = oldPageNumbers.nextSetBit(0); i >= 0; i = oldPageNumbers
371: .nextSetBit(i + 1)) {
372: addPageNumber(oldStartPage + i);
373: }
374:
375: if (newPageNumber != PageChannel.INVALID_PAGE_NUMBER) {
376: // and then add the new page
377: addPageNumber(newPageNumber);
378: }
379: }
380:
381: @Override
382: public String toString() {
383: StringBuilder builder = new StringBuilder(
384: "page numbers (range " + _startPage + " " + _endPage
385: + "): [");
386: PageCursor pCursor = cursor();
387: while (true) {
388: int nextPage = pCursor.getNextPage();
389: if (nextPage < 0) {
390: break;
391: }
392: builder.append(nextPage).append(", ");
393: }
394: builder.append("]");
395: return builder.toString();
396: }
397:
398: private abstract class Handler {
399: protected Handler() {
400: }
401:
402: public boolean containsPageNumber(int pageNumber) {
403: return (isPageWithinRange(pageNumber) && getPageNumbers()
404: .get(pageNumberToBitIndex(pageNumber)));
405: }
406:
407: /**
408: * @param pageNumber Page number to add or remove from this map
409: * @param add True to add it, false to remove it
410: */
411: public abstract void addOrRemovePageNumber(int pageNumber,
412: boolean add) throws IOException;
413: }
414:
415: /**
416: * Usage map whose map is written inline in the same page. For Jet4, this
417: * type of map can usually contains a maximum of 512 pages. Free space maps
418: * are always inline, used space maps may be inline or reference. It has a
419: * start page, which all page numbers in its map are calculated as starting
420: * from.
421: * @author Tim McCune
422: */
423: private class InlineHandler extends Handler {
424: private final boolean _assumeOutOfRangeBitsOn;
425: private final int _maxInlinePages;
426:
427: private InlineHandler(boolean assumeOutOfRangeBitsOn)
428: throws IOException {
429: _assumeOutOfRangeBitsOn = assumeOutOfRangeBitsOn;
430: _maxInlinePages = (getInlineDataEnd() - getInlineDataStart()) * 8;
431: int startPage = getTableBuffer().getInt(getRowStart() + 1);
432: setInlinePageRange(startPage);
433: processMap(getTableBuffer(), 0);
434: }
435:
436: private int getMaxInlinePages() {
437: return _maxInlinePages;
438: }
439:
440: private int getInlineDataStart() {
441: return getRowStart() + getFormat().OFFSET_USAGE_MAP_START;
442: }
443:
444: private int getInlineDataEnd() {
445: return getRowEnd();
446: }
447:
448: /**
449: * Sets the page range for an inline usage map starting from the given
450: * page.
451: */
452: private void setInlinePageRange(int startPage) {
453: setPageRange(startPage, startPage + getMaxInlinePages());
454: }
455:
456: @Override
457: public boolean containsPageNumber(int pageNumber) {
458: return (super .containsPageNumber(pageNumber) || (_assumeOutOfRangeBitsOn
459: && (pageNumber >= 0) && !isPageWithinRange(pageNumber)));
460: }
461:
462: @Override
463: public void addOrRemovePageNumber(int pageNumber, boolean add)
464: throws IOException {
465: if (isPageWithinRange(pageNumber)) {
466:
467: // easy enough, just update the inline data
468: int bufferRelativePageNumber = pageNumberToBitIndex(pageNumber);
469: updateMap(pageNumber, bufferRelativePageNumber,
470: getTableBuffer(), add);
471: // Write the updated map back to disk
472: writeTable();
473:
474: } else {
475:
476: // uh-oh, we've split our britches. what now? determine what our
477: // status is
478: int firstPage = getFirstPageNumber();
479: int lastPage = getLastPageNumber();
480:
481: if (add) {
482:
483: // we can ignore out-of-range page addition if we are already
484: // assuming out-of-range bits are "on". Note, we are leaving small
485: // holes in the database here (leaving behind some free pages), but
486: // it's not the end of the world.
487: if (!_assumeOutOfRangeBitsOn) {
488:
489: // we are adding, can we shift the bits and stay inline?
490: if (firstPage == PageChannel.INVALID_PAGE_NUMBER) {
491: // no pages currently
492: firstPage = pageNumber;
493: lastPage = pageNumber;
494: } else if (pageNumber > lastPage) {
495: lastPage = pageNumber;
496: } else {
497: firstPage = pageNumber;
498: }
499: if ((lastPage - firstPage + 1) < getMaxInlinePages()) {
500:
501: // we can still fit within an inline map
502: moveToNewStartPage(firstPage, pageNumber);
503:
504: } else {
505: // not going to happen, need to promote the usage map to a
506: // reference map
507: promoteInlineHandlerToReferenceHandler(pageNumber);
508: }
509: }
510: } else {
511:
512: // we are removing, what does that mean?
513: if (_assumeOutOfRangeBitsOn) {
514:
515: // we are using an inline map and assuming that anything not
516: // within the current range is "on". so, if we attempt to set a
517: // bit which is before the current page, ignore it, we are not
518: // going back for it.
519: if ((firstPage == PageChannel.INVALID_PAGE_NUMBER)
520: || (pageNumber > lastPage)) {
521:
522: // move to new start page, filling in as we move
523: moveToNewStartPageForRemove(firstPage,
524: lastPage, pageNumber);
525:
526: }
527:
528: } else {
529:
530: // this should not happen, we are removing a page which is not in
531: // the map
532: throw new IOException("Page number "
533: + pageNumber
534: + " already removed from usage map"
535: + ", expected range " + _startPage
536: + " to " + _endPage);
537: }
538: }
539:
540: }
541: }
542:
543: /**
544: * Shifts the inline usage map so that it now starts with the given page.
545: * @param newStartPage new page at which to start
546: * @param newPageNumber optional page number to add once the map has been
547: * shifted to the new start page
548: */
549: private void moveToNewStartPage(int newStartPage,
550: int newPageNumber) throws IOException {
551: int oldStartPage = getStartPage();
552: BitSet oldPageNumbers = (BitSet) getPageNumbers().clone();
553:
554: // clear out the main table (inline usage map data and start page)
555: clearTableAndPages();
556:
557: // write new start page
558: ByteBuffer tableBuffer = getTableBuffer();
559: tableBuffer.position(getRowStart() + 1);
560: tableBuffer.putInt(newStartPage);
561:
562: // write the new table data
563: writeTable();
564:
565: // set new page range
566: setInlinePageRange(newStartPage);
567:
568: // put the pages back in
569: reAddPages(oldStartPage, oldPageNumbers, newPageNumber);
570: }
571:
572: /**
573: * Shifts the inline usage map so that it now starts with the given
574: * firstPage (if valid), otherwise the newPageNumber. Any page numbers
575: * added to the end of the usage map are set to "on".
576: * @param firstPage current first used page
577: * @param lastPage current last used page
578: * @param newPageNumber page number to remove once the map has been
579: * shifted to the new start page
580: */
581: private void moveToNewStartPageForRemove(int firstPage,
582: int lastPage, int newPageNumber) throws IOException {
583: int newStartPage = firstPage;
584: if (newStartPage == PageChannel.INVALID_PAGE_NUMBER) {
585: newStartPage = newPageNumber;
586: } else if ((newPageNumber - newStartPage + 1) >= getMaxInlinePages()) {
587: // this will not move us far enough to hold the new page. just
588: // discard any initial unused pages
589: newStartPage += (newPageNumber - getMaxInlinePages() + 1);
590: }
591:
592: // move the current data
593: moveToNewStartPage(newStartPage,
594: PageChannel.INVALID_PAGE_NUMBER);
595:
596: if (firstPage == PageChannel.INVALID_PAGE_NUMBER) {
597:
598: // this is the common case where we left everything behind
599: ByteUtil.fillRange(_tableBuffer, getInlineDataStart(),
600: getInlineDataEnd());
601:
602: // write out the updated table
603: writeTable();
604:
605: // "add" all the page numbers
606: getPageNumbers().set(0, getMaxInlinePages());
607:
608: } else {
609:
610: // add every new page manually
611: for (int i = (lastPage + 1); i < getEndPage(); ++i) {
612: addPageNumber(i);
613: }
614: }
615:
616: // lastly, remove the new page
617: removePageNumber(newPageNumber);
618: }
619: }
620:
621: /**
622: * Usage map whose map is written across one or more entire separate pages
623: * of page type USAGE_MAP. For Jet4, this type of map can contain 32736
624: * pages per reference page, and a maximum of 17 reference map pages for a
625: * total maximum of 556512 pages (2 GB).
626: * @author Tim McCune
627: */
628: private class ReferenceHandler extends Handler {
629: /** Buffer that contains the current reference map page */
630: private final TempPageHolder _mapPageHolder = TempPageHolder
631: .newHolder(TempBufferHolder.Type.SOFT);
632:
633: private ReferenceHandler() throws IOException {
634: int numUsagePages = (getRowEnd() - getRowStart() - 1) / 4;
635: setStartOffset(getFormat().OFFSET_USAGE_MAP_PAGE_DATA);
636: setPageRange(0, (numUsagePages * getMaxPagesPerUsagePage()));
637:
638: // there is no "start page" for a reference usage map, so we get an
639: // extra page reference on top of the number of page references that fit
640: // in the table
641: for (int i = 0; i < numUsagePages; i++) {
642: int mapPageNum = getTableBuffer().getInt(
643: calculateMapPagePointerOffset(i));
644: if (mapPageNum > 0) {
645: ByteBuffer mapPageBuffer = _mapPageHolder.setPage(
646: getPageChannel(), mapPageNum);
647: byte pageType = mapPageBuffer.get();
648: if (pageType != PageTypes.USAGE_MAP) {
649: throw new IOException(
650: "Looking for usage map at page "
651: + mapPageNum
652: + ", but page type is "
653: + pageType);
654: }
655: mapPageBuffer
656: .position(getFormat().OFFSET_USAGE_MAP_PAGE_DATA);
657: processMap(mapPageBuffer,
658: (getMaxPagesPerUsagePage() * i));
659: }
660: }
661: }
662:
663: private int getMaxPagesPerUsagePage() {
664: return ((getFormat().PAGE_SIZE - getFormat().OFFSET_USAGE_MAP_PAGE_DATA) * 8);
665: }
666:
667: @Override
668: public void addOrRemovePageNumber(int pageNumber, boolean add)
669: throws IOException {
670: if (!isPageWithinRange(pageNumber)) {
671: throw new IOException("Page number " + pageNumber
672: + " is out of supported range");
673: }
674: int pageIndex = (pageNumber / getMaxPagesPerUsagePage());
675: int mapPageNum = getTableBuffer().getInt(
676: calculateMapPagePointerOffset(pageIndex));
677: ByteBuffer mapPageBuffer = null;
678: if (mapPageNum > 0) {
679: mapPageBuffer = _mapPageHolder.setPage(
680: getPageChannel(), mapPageNum);
681: } else {
682: // Need to create a new usage map page
683: mapPageBuffer = createNewUsageMapPage(pageIndex);
684: mapPageNum = _mapPageHolder.getPageNumber();
685: }
686: updateMap(
687: pageNumber,
688: (pageNumber - (getMaxPagesPerUsagePage() * pageIndex)),
689: mapPageBuffer, add);
690: getPageChannel().writePage(mapPageBuffer, mapPageNum);
691: }
692:
693: /**
694: * Create a new usage map page and update the map declaration with a
695: * pointer to it.
696: * @param pageIndex Index of the page reference within the map declaration
697: */
698: private ByteBuffer createNewUsageMapPage(int pageIndex)
699: throws IOException {
700: ByteBuffer mapPageBuffer = _mapPageHolder
701: .setNewPage(getPageChannel());
702: mapPageBuffer.put(PageTypes.USAGE_MAP);
703: mapPageBuffer.put((byte) 0x01); //Unknown
704: mapPageBuffer.putShort((short) 0); //Unknown
705: int mapPageNum = _mapPageHolder.getPageNumber();
706: getTableBuffer().putInt(
707: calculateMapPagePointerOffset(pageIndex),
708: mapPageNum);
709: writeTable();
710: return mapPageBuffer;
711: }
712:
713: private int calculateMapPagePointerOffset(int pageIndex) {
714: return getRowStart()
715: + getFormat().OFFSET_REFERENCE_MAP_PAGE_NUMBERS
716: + (pageIndex * 4);
717: }
718: }
719:
720: /**
721: * Utility class to traverse over the pages in the UsageMap. Remains valid
722: * in the face of usage map modifications.
723: */
724: public final class PageCursor {
725: /** handler for moving the page cursor forward */
726: private final DirHandler _forwardDirHandler = new ForwardDirHandler();
727: /** handler for moving the page cursor backward */
728: private final DirHandler _reverseDirHandler = new ReverseDirHandler();
729: /** the current used page number */
730: private int _curPageNumber;
731: /** the previous used page number */
732: private int _prevPageNumber;
733: /** the last read modification count on the UsageMap. we track this so
734: that the cursor can detect updates to the usage map while traversing
735: and act accordingly */
736: private int _lastModCount;
737:
738: private PageCursor() {
739: reset();
740: }
741:
742: public UsageMap getUsageMap() {
743: return UsageMap.this ;
744: }
745:
746: /**
747: * Returns the DirHandler for the given direction
748: */
749: private DirHandler getDirHandler(boolean moveForward) {
750: return (moveForward ? _forwardDirHandler
751: : _reverseDirHandler);
752: }
753:
754: /**
755: * Returns {@code true} if this cursor is up-to-date with respect to its
756: * usage map.
757: */
758: public boolean isUpToDate() {
759: return (UsageMap.this ._modCount == _lastModCount);
760: }
761:
762: /**
763: * @return valid page number if there was another page to read,
764: * {@link RowId#LAST_PAGE_NUMBER} otherwise
765: */
766: public int getNextPage() {
767: return getAnotherPage(true);
768: }
769:
770: /**
771: * @return valid page number if there was another page to read,
772: * {@link RowId#FIRST_PAGE_NUMBER} otherwise
773: */
774: public int getPreviousPage() {
775: return getAnotherPage(false);
776: }
777:
778: /**
779: * Gets another page in the given direction, returning the new page.
780: */
781: private int getAnotherPage(boolean moveForward) {
782: DirHandler handler = getDirHandler(moveForward);
783: if (_curPageNumber == handler.getEndPageNumber()) {
784: if (!isUpToDate()) {
785: restorePosition(_prevPageNumber);
786: // drop through and retry moving to another page
787: } else {
788: // at end, no more
789: return _curPageNumber;
790: }
791: }
792:
793: checkForModification();
794:
795: _prevPageNumber = _curPageNumber;
796: _curPageNumber = handler
797: .getAnotherPageNumber(_curPageNumber);
798: return _curPageNumber;
799: }
800:
801: /**
802: * After calling this method, getNextPage will return the first page in
803: * the map
804: */
805: public void reset() {
806: beforeFirst();
807: }
808:
809: /**
810: * After calling this method, {@link #getNextPage} will return the first
811: * page in the map
812: */
813: public void beforeFirst() {
814: reset(true);
815: }
816:
817: /**
818: * After calling this method, {@link #getPreviousPage} will return the
819: * last page in the map
820: */
821: public void afterLast() {
822: reset(false);
823: }
824:
825: /**
826: * Resets this page cursor for traversing the given direction.
827: */
828: protected void reset(boolean moveForward) {
829: _curPageNumber = getDirHandler(moveForward)
830: .getBeginningPageNumber();
831: _prevPageNumber = _curPageNumber;
832: _lastModCount = UsageMap.this ._modCount;
833: }
834:
835: /**
836: * Restores a current position for the cursor (current position becomes
837: * previous position).
838: */
839: private void restorePosition(int curPageNumber) {
840: restorePosition(curPageNumber, _curPageNumber);
841: }
842:
843: /**
844: * Restores a current and previous position for the cursor.
845: */
846: protected void restorePosition(int curPageNumber,
847: int prevPageNumber) {
848: if ((curPageNumber != _curPageNumber)
849: || (prevPageNumber != _prevPageNumber)) {
850: _prevPageNumber = updatePosition(prevPageNumber);
851: _curPageNumber = updatePosition(curPageNumber);
852: _lastModCount = UsageMap.this ._modCount;
853: } else {
854: checkForModification();
855: }
856: }
857:
858: /**
859: * Checks the usage map for modifications an updates state accordingly.
860: */
861: private void checkForModification() {
862: if (!isUpToDate()) {
863: _prevPageNumber = updatePosition(_prevPageNumber);
864: _curPageNumber = updatePosition(_curPageNumber);
865: _lastModCount = UsageMap.this ._modCount;
866: }
867: }
868:
869: private int updatePosition(int pageNumber) {
870: if (pageNumber < UsageMap.this .getFirstPageNumber()) {
871: pageNumber = RowId.FIRST_PAGE_NUMBER;
872: } else if (pageNumber > UsageMap.this .getLastPageNumber()) {
873: pageNumber = RowId.LAST_PAGE_NUMBER;
874: }
875: return pageNumber;
876: }
877:
878: @Override
879: public String toString() {
880: return getClass().getSimpleName() + " CurPosition "
881: + _curPageNumber + ", PrevPosition "
882: + _prevPageNumber;
883: }
884:
885: /**
886: * Handles moving the cursor in a given direction. Separates cursor
887: * logic from value storage.
888: */
889: private abstract class DirHandler {
890: public abstract int getAnotherPageNumber(int curPageNumber);
891:
892: public abstract int getBeginningPageNumber();
893:
894: public abstract int getEndPageNumber();
895: }
896:
897: /**
898: * Handles moving the cursor forward.
899: */
900: private final class ForwardDirHandler extends DirHandler {
901: @Override
902: public int getAnotherPageNumber(int curPageNumber) {
903: if (curPageNumber == getBeginningPageNumber()) {
904: return UsageMap.this .getFirstPageNumber();
905: }
906: return UsageMap.this .getNextPageNumber(curPageNumber);
907: }
908:
909: @Override
910: public int getBeginningPageNumber() {
911: return RowId.FIRST_PAGE_NUMBER;
912: }
913:
914: @Override
915: public int getEndPageNumber() {
916: return RowId.LAST_PAGE_NUMBER;
917: }
918: }
919:
920: /**
921: * Handles moving the cursor backward.
922: */
923: private final class ReverseDirHandler extends DirHandler {
924: @Override
925: public int getAnotherPageNumber(int curPageNumber) {
926: if (curPageNumber == getBeginningPageNumber()) {
927: return UsageMap.this .getLastPageNumber();
928: }
929: return UsageMap.this .getPrevPageNumber(curPageNumber);
930: }
931:
932: @Override
933: public int getBeginningPageNumber() {
934: return RowId.LAST_PAGE_NUMBER;
935: }
936:
937: @Override
938: public int getEndPageNumber() {
939: return RowId.FIRST_PAGE_NUMBER;
940: }
941: }
942:
943: }
944:
945: }
|