View Javadoc

1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   This library is free software; you can redistribute it and/or
5   modify it under the terms of the GNU Lesser General Public
6   License as published by the Free Software Foundation; either
7   version 2.1 of the License, or (at your option) any later version.
8   
9   This library is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  Lesser General Public License for more details.
13  
14  You should have received a copy of the GNU Lesser General Public
15  License along with this library; if not, write to the Free Software
16  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
17  USA
18  
19  You can contact Health Market Science at info@healthmarketscience.com
20  or at the following address:
21  
22  Health Market Science
23  2700 Horizon Drive
24  Suite 200
25  King of Prussia, PA 19406
26  */
27  
28  package com.healthmarketscience.jackcess;
29  
30  import java.io.ByteArrayOutputStream;
31  import java.io.IOException;
32  import java.nio.ByteBuffer;
33  import java.nio.ByteOrder;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.Comparator;
38  import java.util.Iterator;
39  import java.util.LinkedList;
40  import java.util.List;
41  import java.util.Map;
42  
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  
46  import static com.healthmarketscience.jackcess.IndexCodes.*;
47  
48  
49  /**
50   * Access table index
51   * @author Tim McCune
52   */
53  public abstract class Index implements Comparable<Index> {
54    
55    protected static final Log LOG = LogFactory.getLog(Index.class);
56  
57    /** special entry which is less than any other entry */
58    public static final Entry FIRST_ENTRY =
59      createSpecialEntry(RowId.FIRST_ROW_ID);
60    
61    /** special entry which is greater than any other entry */
62    public static final Entry LAST_ENTRY =
63      createSpecialEntry(RowId.LAST_ROW_ID);
64    
65    protected static final int INVALID_INDEX_PAGE_NUMBER = 0;          
66    
67    /** Max number of columns in an index */
68    private static final int MAX_COLUMNS = 10;
69    
70    protected static final byte[] EMPTY_PREFIX = new byte[0];
71  
72    private static final short COLUMN_UNUSED = -1;
73  
74    private static final byte ASCENDING_COLUMN_FLAG = (byte)0x01;
75  
76    private static final byte UNIQUE_INDEX_FLAG = (byte)0x01;
77    private static final byte IGNORE_NULLS_INDEX_FLAG = (byte)0x02;
78  
79    /** index type for primary key indexes */
80    private static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1;
81    
82    /** index type for foreign key indexes */
83    private static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2;
84  
85    private static final int MAX_TEXT_INDEX_CHAR_LENGTH =
86      (JetFormat.TEXT_FIELD_MAX_LENGTH / JetFormat.TEXT_FIELD_UNIT_SIZE);
87    
88    /** type attributes for Entries which simplify comparisons */
89    public enum EntryType {
90      /** comparable type indicating this Entry should always compare less than
91          valid RowIds */
92      ALWAYS_FIRST,
93      /** comparable type indicating this Entry should always compare less than
94          other valid entries with equal entryBytes */
95      FIRST_VALID,
96      /** comparable type indicating this RowId should always compare
97          normally */
98      NORMAL,
99      /** comparable type indicating this Entry should always compare greater
100         than other valid entries with equal entryBytes */
101     LAST_VALID,
102     /** comparable type indicating this Entry should always compare greater
103         than valid RowIds */
104     ALWAYS_LAST;
105   }
106   
107   static final Comparator<byte[]> BYTE_CODE_COMPARATOR =
108     new Comparator<byte[]>() {
109       public int compare(byte[] left, byte[] right) {
110         if(left == right) {
111           return 0;
112         }
113         if(left == null) {
114           return -1;
115         }
116         if(right == null) {
117           return 1;
118         }
119 
120         int len = Math.min(left.length, right.length);
121         int pos = 0;
122         while((pos < len) && (left[pos] == right[pos])) {
123           ++pos;
124         }
125         if(pos < len) {
126           return ((ByteUtil.asUnsignedByte(left[pos]) <
127                    ByteUtil.asUnsignedByte(right[pos])) ? -1 : 1);
128         }
129         return ((left.length < right.length) ? -1 :
130                 ((left.length > right.length) ? 1 : 0));
131       }
132     };
133         
134   
135   /** owning table */
136   private final Table _table;
137   /** Page number of the root index data */
138   private int _rootPageNumber;
139   /** offset within the tableDefinition buffer of the uniqueEntryCount for
140       this index */
141   private final int _uniqueEntryCountOffset;
142   /** The number of unique entries which have been added to this index.  note,
143       however, that it is never decremented, only incremented (as observed in
144       Access). */
145   private int _uniqueEntryCount;
146   /** List of columns and flags */
147   private final List<ColumnDescriptor> _columns =
148     new ArrayList<ColumnDescriptor>();
149   /** 0-based index number */
150   private int _indexNumber;
151   /** flags for this index */
152   private byte _indexFlags;
153   /** the type of the index */
154   private byte _indexType;
155   /** Index name */
156   private String _name;
157   /** <code>true</code> if the index entries have been initialized,
158       <code>false</code> otherwise */
159   private boolean _initialized;
160   /** modification count for the table, keeps cursors up-to-date */
161   private int _modCount;
162   /** temp buffer used to read/write the index pages */
163   private final TempBufferHolder _indexBufferH =
164     TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true);
165   /** max size for all the entries written to a given index data page */
166   private final int _maxPageEntrySize;
167   /** FIXME, for now, we can't write multi-page indexes or indexes using the funky primary key compression scheme */
168   boolean _readOnly;
169   
170   protected Index(Table table, int uniqueEntryCount,
171                   int uniqueEntryCountOffset)
172   {
173     _table  = table;
174     _uniqueEntryCount = uniqueEntryCount;
175     _uniqueEntryCountOffset = uniqueEntryCountOffset;
176     _maxPageEntrySize = calcMaxPageEntrySize(_table.getFormat());
177   }
178 
179   public Table getTable() {
180     return _table;
181   }
182   
183   public JetFormat getFormat() {
184     return getTable().getFormat();
185   }
186 
187   public PageChannel getPageChannel() {
188     return getTable().getPageChannel();
189   }
190 
191   public void setIndexNumber(int indexNumber) {
192     _indexNumber = indexNumber;
193   }
194 
195   public int getIndexNumber() {
196     return _indexNumber;
197   }
198 
199   public void setIndexType(byte indexType) {
200     _indexType = indexType;
201   }
202 
203   public byte getIndexFlags() {
204     return _indexFlags;
205   }
206   
207   public int getUniqueEntryCount() {
208     return _uniqueEntryCount;
209   }
210 
211   public int getUniqueEntryCountOffset() {
212     return _uniqueEntryCountOffset;
213   }
214 
215   public String getName() {
216     return _name;
217   }
218   
219   public void setName(String name) {
220     _name = name;
221   }
222 
223   public boolean isPrimaryKey() {
224     return _indexType == PRIMARY_KEY_INDEX_TYPE;
225   }
226 
227   public boolean isForeignKey() {
228     return _indexType == FOREIGN_KEY_INDEX_TYPE;
229   }
230 
231   /**
232    * Whether or not {@code null} values are actually recorded in the index.
233    */
234   public boolean shouldIgnoreNulls() {
235     return((_indexFlags & IGNORE_NULLS_INDEX_FLAG) != 0);
236   }
237 
238   /**
239    * Whether or not index entries must be unique.
240    * <p>
241    * Some notes about uniqueness:
242    * <ul>
243    * <li>Access does not seem to consider multiple {@code null} entries
244    *     invalid for a unique index</li>
245    * <li>text indexes collapse case, and Access seems to compare <b>only</b>
246    *     the index entry bytes, therefore two strings which differ only in
247    *     case <i>will violate</i> the unique constraint</li>
248    * </ul>
249    */
250   public boolean isUnique() {
251     return(isPrimaryKey() || ((_indexFlags & UNIQUE_INDEX_FLAG) != 0));
252   }
253   
254   /**
255    * Returns the Columns for this index (unmodifiable)
256    */
257   public List<ColumnDescriptor> getColumns() {
258     return Collections.unmodifiableList(_columns);
259   }
260 
261   /**
262    * Whether or not the complete index state has been read.
263    */
264   public boolean isInitialized() {
265     return _initialized;
266   }
267 
268   protected int getRootPageNumber() {
269     return _rootPageNumber;
270   }
271 
272   protected void setReadOnly() {
273     _readOnly = true;
274   }
275 
276   protected int getMaxPageEntrySize() {
277     return _maxPageEntrySize;
278   }
279 
280   /**
281    * Returns the number of index entries in the index.  Only called by unit
282    * tests.
283    * <p>
284    * Forces index initialization.
285    */
286   protected int getEntryCount()
287     throws IOException
288   {
289     initialize();
290     EntryCursor cursor = cursor();
291     Entry endEntry = cursor.getLastEntry();
292     int count = 0;
293     while(!endEntry.equals(cursor.getNextEntry())) {
294       ++count;
295     }
296     return count;
297   }
298   
299   /**
300    * Forces initialization of this index (actual parsing of index pages).
301    * normally, the index will not be initialized until the entries are
302    * actually needed.
303    */
304   public void initialize() throws IOException {
305     if(!_initialized) {
306       readIndexEntries();
307       _initialized = true;
308     }
309   }
310 
311   /**
312    * Writes the current index state to the database.
313    * <p>
314    * Forces index initialization.
315    */
316   public void update() throws IOException
317   {
318     // make sure we've parsed the entries
319     initialize();
320     
321     if(_readOnly) {
322       throw new UnsupportedOperationException(
323           "FIXME cannot write indexes of this type yet, see Database javadoc for info on enabling large index support");
324     }
325     updateImpl();
326   }
327 
328   /**
329    * Read the index info from a tableBuffer
330    * @param tableBuffer table definition buffer to read from initial info
331    * @param availableColumns Columns that this index may use
332    */
333   public void read(ByteBuffer tableBuffer, List<Column> availableColumns)
334     throws IOException
335   {
336     for (int i = 0; i < MAX_COLUMNS; i++) {
337       short columnNumber = tableBuffer.getShort();
338       byte colFlags = tableBuffer.get();
339       if (columnNumber != COLUMN_UNUSED) {
340         // find the desired column by column number (which is not necessarily
341         // the same as the column index)
342         Column idxCol = null;
343         for(Column col : availableColumns) {
344           if(col.getColumnNumber() == columnNumber) {
345             idxCol = col;
346             break;
347           }
348         }
349         if(idxCol == null) {
350           throw new IOException("Could not find column with number "
351                                 + columnNumber + " for index " + getName());
352         }
353         _columns.add(newColumnDescriptor(idxCol, colFlags));
354       }
355     }
356     tableBuffer.getInt(); //Forward past Unknown
357     _rootPageNumber = tableBuffer.getInt();
358     tableBuffer.getInt(); //Forward past Unknown
359     _indexFlags = tableBuffer.get();
360     tableBuffer.position(tableBuffer.position() + 5);  //Forward past other stuff
361   }
362 
363   /**
364    * Adds a row to this index
365    * <p>
366    * Forces index initialization.
367    * 
368    * @param row Row to add
369    * @param rowId rowId of the row to be added
370    */
371   public void addRow(Object[] row, RowId rowId)
372     throws IOException
373   {
374     int nullCount = countNullValues(row);
375     boolean isNullEntry = (nullCount == _columns.size());
376     if(shouldIgnoreNulls() && isNullEntry) {
377       // nothing to do
378       return;
379     }
380     if(isPrimaryKey() && (nullCount > 0)) {
381       throw new IOException("Null value found in row " + Arrays.asList(row)
382                             + " for primary key index " + this);
383     }
384     
385     // make sure we've parsed the entries
386     initialize();
387 
388     Entry newEntry = new Entry(createEntryBytes(row), rowId);
389     if(addEntry(newEntry, isNullEntry, row)) {
390       ++_modCount;
391     } else {
392       LOG.warn("Added duplicate index entry " + newEntry + " for row: " +
393                Arrays.asList(row));
394     }
395   }
396 
397   /**
398    * Adds an entry to the correct index dataPage, maintaining the order.
399    */
400   private boolean addEntry(Entry newEntry, boolean isNullEntry, Object[] row)
401     throws IOException
402   {
403     DataPage dataPage = findDataPage(newEntry);
404     int idx = dataPage.findEntry(newEntry);
405     if(idx < 0) {
406       // this is a new entry
407       idx = missingIndexToInsertionPoint(idx);
408 
409       Position newPos = new Position(dataPage, idx, newEntry, true);
410       Position nextPos = getNextPosition(newPos);
411       Position prevPos = getPreviousPosition(newPos);
412       
413       // determine if the addition of this entry would break the uniqueness
414       // constraint.  See isUnique() for some notes about uniqueness as
415       // defined by Access.
416       boolean isDupeEntry =
417         (((nextPos != null) &&
418           newEntry.equalsEntryBytes(nextPos.getEntry())) ||
419           ((prevPos != null) &&
420            newEntry.equalsEntryBytes(prevPos.getEntry())));
421       if(isUnique() && !isNullEntry && isDupeEntry)
422       {
423         throw new IOException(
424             "New row " + Arrays.asList(row) +
425             " violates uniqueness constraint for index " + this);
426       }
427 
428       if(!isDupeEntry) {
429         ++_uniqueEntryCount;
430       }
431 
432       dataPage.addEntry(idx, newEntry);
433       return true;
434     }
435     return false;
436   }
437   
438   /**
439    * Removes a row from this index
440    * <p>
441    * Forces index initialization.
442    * 
443    * @param row Row to remove
444    * @param rowId rowId of the row to be removed
445    */
446   public void deleteRow(Object[] row, RowId rowId)
447     throws IOException
448   {
449     int nullCount = countNullValues(row);
450     if(shouldIgnoreNulls() && (nullCount == _columns.size())) {
451       // nothing to do
452       return;
453     }
454     
455     // make sure we've parsed the entries
456     initialize();
457 
458     Entry oldEntry = new Entry(createEntryBytes(row), rowId);
459     if(removeEntry(oldEntry)) {
460       ++_modCount;
461     } else {
462       LOG.warn("Failed removing index entry " + oldEntry + " for row: " +
463                Arrays.asList(row));
464     }
465   }
466 
467   /**
468    * Removes an entry from the relevant index dataPage, maintaining the order.
469    * Will search by RowId if entry is not found (in case a partial entry was
470    * provided).
471    */
472   private boolean removeEntry(Entry oldEntry)
473     throws IOException
474   {
475     DataPage dataPage = findDataPage(oldEntry);
476     int idx = dataPage.findEntry(oldEntry);
477     boolean doRemove = false;
478     if(idx < 0) {
479       // the caller may have only read some of the row data, if this is the
480       // case, just search for the page/row numbers
481       // FIXME, we could force caller to get relevant values?
482       EntryCursor cursor = cursor();
483       Position tmpPos = null;
484       Position endPos = cursor._lastPos;
485       while(!endPos.equals(
486                 tmpPos = cursor.getAnotherPosition(Cursor.MOVE_FORWARD))) {
487         if(tmpPos.getEntry().getRowId().equals(oldEntry.getRowId())) {
488           dataPage = tmpPos.getDataPage();
489           idx = tmpPos.getIndex();
490           doRemove = true;
491           break;
492         }
493       }
494     } else {
495       doRemove = true;
496     }
497 
498     if(doRemove) {
499       // found it!
500       dataPage.removeEntry(idx);
501     }
502     
503     return doRemove;
504   }
505       
506   /**
507    * Gets a new cursor for this index.
508    * <p>
509    * Forces index initialization.
510    */
511   public EntryCursor cursor()
512     throws IOException
513   {
514     return cursor(null, true, null, true);
515   }
516   
517   /**
518    * Gets a new cursor for this index, narrowed to the range defined by the
519    * given startRow and endRow.
520    * <p>
521    * Forces index initialization.
522    * 
523    * @param startRow the first row of data for the cursor, or {@code null} for
524    *                 the first entry
525    * @param startInclusive whether or not startRow is inclusive or exclusive
526    * @param endRow the last row of data for the cursor, or {@code null} for
527    *               the last entry
528    * @param endInclusive whether or not endRow is inclusive or exclusive
529    */
530   public EntryCursor cursor(Object[] startRow,
531                             boolean startInclusive,
532                             Object[] endRow,
533                             boolean endInclusive)
534     throws IOException
535   {
536     initialize();
537     Entry startEntry = FIRST_ENTRY;
538     byte[] startEntryBytes = null;
539     if(startRow != null) {
540       startEntryBytes = createEntryBytes(startRow);
541       startEntry = new Entry(startEntryBytes,
542                              (startInclusive ?
543                               RowId.FIRST_ROW_ID : RowId.LAST_ROW_ID));
544     }
545     Entry endEntry = LAST_ENTRY;
546     if(endRow != null) {
547       // reuse startEntryBytes if startRow and endRow are same array.  this is
548       // common for "lookup" code
549       byte[] endEntryBytes = ((startRow == endRow) ?
550                               startEntryBytes :
551                               createEntryBytes(endRow));
552       endEntry = new Entry(endEntryBytes,
553                            (endInclusive ?
554                             RowId.LAST_ROW_ID : RowId.FIRST_ROW_ID));
555     }
556     return new EntryCursor(findEntryPosition(startEntry),
557                            findEntryPosition(endEntry));
558   }
559 
560   private Position findEntryPosition(Entry entry)
561     throws IOException
562   {
563     DataPage dataPage = findDataPage(entry);
564     int idx = dataPage.findEntry(entry);
565     boolean between = false;
566     if(idx < 0) {
567       // given entry was not found exactly.  our current position is now
568       // really between two indexes, but we cannot support that as an integer
569       // value, so we set a flag instead
570       idx = missingIndexToInsertionPoint(idx);
571       between = true;
572     }
573     return new Position(dataPage, idx, entry, between);
574   }
575 
576   private Position getNextPosition(Position curPos)
577     throws IOException
578   {
579     // get the next index (between-ness is handled internally)
580     int nextIdx = curPos.getNextIndex();
581     Position nextPos = null;
582     if(nextIdx < curPos.getDataPage().getEntries().size()) {
583       nextPos = new Position(curPos.getDataPage(), nextIdx);
584     } else {
585       int nextPageNumber = curPos.getDataPage().getNextPageNumber();
586       DataPage nextDataPage = null;
587       while(nextPageNumber != INVALID_INDEX_PAGE_NUMBER) {
588         DataPage dp = getDataPage(nextPageNumber);
589         if(!dp.isEmpty()) {
590           nextDataPage = dp;
591           break;
592         }
593         nextPageNumber = dp.getNextPageNumber();
594       }
595       if(nextDataPage != null) {
596         nextPos = new Position(nextDataPage, 0);
597       }
598     }
599     return nextPos;
600   }
601 
602   /**
603    * Returns the Position before the given one, or {@code null} if none.
604    */
605   private Position getPreviousPosition(Position curPos)
606     throws IOException
607   {
608     // get the previous index (between-ness is handled internally)
609     int prevIdx = curPos.getPrevIndex();
610     Position prevPos = null;
611     if(prevIdx >= 0) {
612       prevPos = new Position(curPos.getDataPage(), prevIdx);
613     } else {
614       int prevPageNumber = curPos.getDataPage().getPrevPageNumber();
615       DataPage prevDataPage = null;
616       while(prevPageNumber != INVALID_INDEX_PAGE_NUMBER) {
617         DataPage dp = getDataPage(prevPageNumber);
618         if(!dp.isEmpty()) {
619           prevDataPage = dp;
620           break;
621         }
622         prevPageNumber = dp.getPrevPageNumber();
623       }
624       if(prevDataPage != null) {
625         prevPos = new Position(prevDataPage,
626                                (prevDataPage.getEntries().size() - 1));
627       }
628     }
629     return prevPos;
630   }
631 
632   /**
633    * Returns the valid insertion point for an index indicating a missing
634    * entry.
635    */
636   protected static int missingIndexToInsertionPoint(int idx) {
637     return -(idx + 1);
638   }
639 
640   /**
641    * Constructs an array of values appropriate for this index from the given
642    * column values, expected to match the columns for this index.
643    * @return the appropriate sparse array of data
644    * @throws IllegalArgumentException if the wrong number of values are
645    *         provided
646    */
647   public Object[] constructIndexRowFromEntry(Object... values)
648   {
649     if(values.length != _columns.size()) {
650       throw new IllegalArgumentException(
651           "Wrong number of column values given " + values.length +
652           ", expected " + _columns.size());
653     }
654     int valIdx = 0;
655     Object[] idxRow = new Object[getTable().getColumnCount()];
656     for(ColumnDescriptor col : _columns) {
657       idxRow[col.getColumnIndex()] = values[valIdx++];
658     }
659     return idxRow;
660   }
661     
662   /**
663    * Constructs an array of values appropriate for this index from the given
664    * column value.
665    * @return the appropriate sparse array of data or {@code null} if not all
666    *         columns for this index were provided
667    */
668   public Object[] constructIndexRow(String colName, Object value)
669   {
670     return constructIndexRow(Collections.singletonMap(colName, value));
671   }
672   
673   /**
674    * Constructs an array of values appropriate for this index from the given
675    * column values.
676    * @return the appropriate sparse array of data or {@code null} if not all
677    *         columns for this index were provided
678    */
679   public Object[] constructIndexRow(Map<String,Object> row)
680   {
681     for(ColumnDescriptor col : _columns) {
682       if(!row.containsKey(col.getName())) {
683         return null;
684       }
685     }
686 
687     Object[] idxRow = new Object[getTable().getColumnCount()];
688     for(ColumnDescriptor col : _columns) {
689       idxRow[col.getColumnIndex()] = row.get(col.getName());
690     }
691     return idxRow;
692   }  
693 
694   @Override
695   public String toString() {
696     StringBuilder rtn = new StringBuilder();
697     rtn.append("\tName: (" + _table.getName() + ") " + _name);
698     rtn.append("\n\tNumber: " + _indexNumber);
699     rtn.append("\n\tPage number: " + _rootPageNumber);
700     rtn.append("\n\tIs Primary Key: " + isPrimaryKey());
701     rtn.append("\n\tIs Foreign Key: " + isForeignKey());
702     rtn.append("\n\tIs Unique: " + isUnique());
703     rtn.append("\n\tIgnore Nulls: " + shouldIgnoreNulls());
704     rtn.append("\n\tColumns: " + _columns);
705     rtn.append("\n\tInitialized: " + _initialized);
706     if(_initialized) {
707       try {
708         rtn.append("\n\tEntryCount: " + getEntryCount());
709       } catch(IOException e) {
710         throw new RuntimeException(e);
711       }
712     }
713     rtn.append("\n\n");
714     return rtn.toString();
715   }
716   
717   public int compareTo(Index other) {
718     if (_indexNumber > other.getIndexNumber()) {
719       return 1;
720     } else if (_indexNumber < other.getIndexNumber()) {
721       return -1;
722     } else {
723       return 0;
724     }
725   }
726 
727   /**
728    * Write the given index page out to a buffer
729    */
730   protected void writeDataPage(DataPage dataPage)
731     throws IOException
732   {
733     if(dataPage.getCompressedEntrySize() > _maxPageEntrySize) {
734       if(this instanceof SimpleIndex) {
735         throw new UnsupportedOperationException(
736             "FIXME cannot write large index yet, see Database javadoc for info on enabling large index support");
737       }
738       throw new IllegalStateException("data page is too large");
739     }
740     
741     ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());
742     buffer.put(dataPage.isLeaf() ?
743                PageTypes.INDEX_LEAF :
744                PageTypes.INDEX_NODE );  //Page type
745     buffer.put((byte) 0x01);  //Unknown
746     buffer.putShort((short) 0); //Free space
747     buffer.putInt(getTable().getTableDefPageNumber());
748 
749     buffer.putInt(0); //Unknown
750     buffer.putInt(dataPage.getPrevPageNumber()); //Prev page
751     buffer.putInt(dataPage.getNextPageNumber()); //Next page
752     buffer.putInt(dataPage.getChildTailPageNumber()); //ChildTail page
753 
754     byte[] entryPrefix = dataPage.getEntryPrefix();
755     buffer.putShort((short) entryPrefix.length); // entry prefix byte count
756     buffer.put((byte) 0); //Unknown
757 
758     byte[] entryMask = new byte[getFormat().SIZE_INDEX_ENTRY_MASK];
759     // first entry includes the prefix
760     int totalSize = entryPrefix.length;
761     for(Entry entry : dataPage.getEntries()) {
762       totalSize += (entry.size() - entryPrefix.length);
763       int idx = totalSize / 8;
764       entryMask[idx] |= (1 << (totalSize % 8));
765     }
766     buffer.put(entryMask);
767 
768     // first entry includes the prefix
769     buffer.put(entryPrefix);
770     
771     for(Entry entry : dataPage.getEntries()) {
772       entry.write(buffer, entryPrefix);
773     }
774 
775     // update free space
776     buffer.putShort(2, (short) (getFormat().PAGE_SIZE - buffer.position()));
777 
778     getPageChannel().writePage(buffer, dataPage.getPageNumber());
779   }
780 
781   /**
782    * Reads an index page, populating the correct collection based on the page
783    * type (node or leaf).
784    */
785   protected void readDataPage(DataPage dataPage)
786     throws IOException
787   {
788     ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());
789     getPageChannel().readPage(buffer, dataPage.getPageNumber());
790 
791     boolean isLeaf = isLeafPage(buffer);
792     dataPage.setLeaf(isLeaf);
793 
794     // note, "header" data is in LITTLE_ENDIAN format, entry data is in
795     // BIG_ENDIAN format
796     int entryPrefixLength = ByteUtil.getUnsignedShort(
797         buffer, getFormat().OFFSET_INDEX_COMPRESSED_BYTE_COUNT);
798     int entryMaskLength = getFormat().SIZE_INDEX_ENTRY_MASK;
799     int entryMaskPos = getFormat().OFFSET_INDEX_ENTRY_MASK;
800     int entryPos = entryMaskPos + entryMaskLength;
801     int lastStart = 0;
802     int totalEntrySize = 0;
803     byte[] entryPrefix = null;
804     List<Entry> entries = new ArrayList<Entry>();
805     TempBufferHolder tmpEntryBufferH =
806       TempBufferHolder.newHolder(TempBufferHolder.Type.HARD, true,
807                                  ByteOrder.BIG_ENDIAN);
808 
809     Entry prevEntry = FIRST_ENTRY;
810     for (int i = 0; i < entryMaskLength; i++) {
811       byte entryMask = buffer.get(entryMaskPos + i);
812       for (int j = 0; j < 8; j++) {
813         if ((entryMask & (1 << j)) != 0) {
814           int length = (i * 8) + j - lastStart;
815           buffer.position(entryPos + lastStart);
816 
817           // determine if we can read straight from the index page (if no
818           // entryPrefix).  otherwise, create temp buf with complete entry.
819           ByteBuffer curEntryBuffer = buffer;
820           int curEntryLen = length;
821           if(entryPrefix != null) {
822             curEntryBuffer = getTempEntryBuffer(
823                 buffer, length, entryPrefix, tmpEntryBufferH);
824             curEntryLen += entryPrefix.length;
825           }
826           totalEntrySize += curEntryLen;
827 
828           Entry entry = newEntry(curEntryBuffer, curEntryLen, isLeaf);
829           if(prevEntry.compareTo(entry) >= 0) {
830             throw new IOException("Unexpected order in index entries, " +
831                                   prevEntry + " >= " + entry);
832           }
833           
834           entries.add(entry);
835 
836           if((entries.size() == 1) && (entryPrefixLength > 0)) {
837             // read any shared entry prefix
838             entryPrefix = new byte[entryPrefixLength];
839             buffer.position(entryPos + lastStart);
840             buffer.get(entryPrefix);
841           }
842 
843           lastStart += length;
844           prevEntry = entry;
845         }
846       }
847     }
848 
849     dataPage.setEntryPrefix(entryPrefix != null ? entryPrefix : EMPTY_PREFIX);
850     dataPage.setEntries(entries);
851     dataPage.setTotalEntrySize(totalEntrySize);
852     
853     int prevPageNumber = buffer.getInt(getFormat().OFFSET_PREV_INDEX_PAGE);
854     int nextPageNumber = buffer.getInt(getFormat().OFFSET_NEXT_INDEX_PAGE);
855     int childTailPageNumber =
856       buffer.getInt(getFormat().OFFSET_CHILD_TAIL_INDEX_PAGE);
857 
858     dataPage.setPrevPageNumber(prevPageNumber);
859     dataPage.setNextPageNumber(nextPageNumber);
860     dataPage.setChildTailPageNumber(childTailPageNumber);
861   }
862 
863   /**
864    * Returns a new Entry of the correct type for the given data and page type.
865    */
866   private Entry newEntry(ByteBuffer buffer, int entryLength, boolean isLeaf)
867     throws IOException
868   {
869     if(isLeaf) {
870       return new Entry(buffer, entryLength);
871     }
872     return new NodeEntry(buffer, entryLength);
873   }
874 
875   /**
876    * Returns an entry buffer containing the relevant data for an entry given
877    * the valuePrefix.
878    */
879   private ByteBuffer getTempEntryBuffer(
880       ByteBuffer indexPage, int entryLen, byte[] valuePrefix,
881       TempBufferHolder tmpEntryBufferH)
882   {
883     ByteBuffer tmpEntryBuffer = tmpEntryBufferH.getBuffer(
884         getPageChannel(), valuePrefix.length + entryLen);
885 
886     // combine valuePrefix and rest of entry from indexPage, then prep for
887     // reading
888     tmpEntryBuffer.put(valuePrefix);
889     tmpEntryBuffer.put(indexPage.array(), indexPage.position(), entryLen);
890     tmpEntryBuffer.flip();
891     
892     return tmpEntryBuffer;
893   }
894     
895   /**
896    * Determines if the given index page is a leaf or node page.
897    */
898   private static boolean isLeafPage(ByteBuffer buffer)
899     throws IOException
900   {
901     byte pageType = buffer.get(0);
902     if(pageType == PageTypes.INDEX_LEAF) {
903       return true;
904     } else if(pageType == PageTypes.INDEX_NODE) {
905       return false;
906     }
907     throw new IOException("Unexpected page type " + pageType);
908   }
909   
910   /**
911    * Determines the number of {@code null} values for this index from the
912    * given row.
913    */
914   private int countNullValues(Object[] values)
915   {
916     if(values == null) {
917       return _columns.size();
918     }
919     
920     // annoyingly, the values array could come from different sources, one
921     // of which will make it a different size than the other.  we need to
922     // handle both situations.
923     int nullCount = 0;
924     for(ColumnDescriptor col : _columns) {
925       Object value = values[col.getColumnIndex()];
926       if(col.isNullValue(value)) {
927         ++nullCount;
928       }
929     }
930     
931     return nullCount;
932   }
933 
934   /**
935    * Creates the entry bytes for a row of values.
936    */
937   private byte[] createEntryBytes(Object[] values) throws IOException
938   {
939     if(values == null) {
940       return null;
941     }
942     
943     ByteArrayOutputStream bout = new ByteArrayOutputStream();
944     
945     // annoyingly, the values array could come from different sources, one
946     // of which will make it a different size than the other.  we need to
947     // handle both situations.
948     for(ColumnDescriptor col : _columns) {
949       Object value = values[col.getColumnIndex()];
950       col.writeValue(value, bout);
951     }
952     
953     return bout.toByteArray();
954   }  
955   
956   /**
957    * Writes the current index state to the database.  Index has already been
958    * initialized.
959    */
960   protected abstract void updateImpl() throws IOException;
961   
962   /**
963    * Reads the actual index entries.
964    */
965   protected abstract void readIndexEntries()
966     throws IOException;
967 
968   /**
969    * Finds the data page for the given entry.
970    */
971   protected abstract DataPage findDataPage(Entry entry)
972     throws IOException;
973   
974   /**
975    * Gets the data page for the pageNumber.
976    */
977   protected abstract DataPage getDataPage(int pageNumber)
978     throws IOException;
979   
980   /**
981    * Flips the first bit in the byte at the given index.
982    */
983   private static byte[] flipFirstBitInByte(byte[] value, int index)
984   {
985     value[index] = (byte)(value[index] ^ 0x80);
986 
987     return value;
988   }
989 
990   /**
991    * Flips all the bits in the byte array.
992    */
993   private static byte[] flipBytes(byte[] value) {
994     for(int i = 0; i < value.length; ++i) {
995       value[i] = (byte)(~value[i]);
996     } 
997     return value;
998   }
999 
1000   /**
1001    * Writes the value of the given column type to a byte array and returns it.
1002    */
1003   private static byte[] encodeNumberColumnValue(Object value, Column column)
1004     throws IOException
1005   {
1006     // always write in big endian order
1007     return column.write(value, 0, ByteOrder.BIG_ENDIAN).array();
1008   }    
1009 
1010   /**
1011    * Converts an index value for a text column into the entry value (which
1012    * is based on a variety of nifty codes).
1013    */
1014   private static void writeNonNullIndexTextValue(
1015       Object value, ByteArrayOutputStream bout, boolean isAscending)
1016     throws IOException
1017   {
1018     // first, convert to string
1019     String str = Column.toCharSequence(value).toString();
1020 
1021     // all text columns (including memos) are only indexed up to the max
1022     // number of chars in a VARCHAR column
1023     if(str.length() > MAX_TEXT_INDEX_CHAR_LENGTH) {
1024       str = str.substring(0, MAX_TEXT_INDEX_CHAR_LENGTH);
1025     }
1026     
1027     ByteArrayOutputStream tmpBout = bout;
1028     if(!isAscending) {
1029       // we need to accumulate the bytes in a temp array in order to negate
1030       // them before writing them to the final array
1031       tmpBout = new ByteArrayOutputStream();
1032     }
1033     
1034     // now, convert each character to a "code" of one or more bytes
1035     List<ExtraCodes> unprintableCodes = null;
1036     List<ExtraCodes> internationalCodes = null;
1037     int charOffset = 0;
1038     for(int i = 0; i < str.length(); ++i) {
1039       char c = str.charAt(i);
1040       Character cKey = c;
1041 
1042       byte[] bytes = CODES.get(cKey);
1043       if(bytes != null) {
1044         // simple case, write the codes we found
1045         tmpBout.write(bytes);
1046         ++charOffset;
1047         continue;
1048       }
1049 
1050       bytes = UNPRINTABLE_CODES.get(cKey);
1051       if(bytes != null) {
1052         // we do not write anything to tmpBout
1053         if(bytes.length > 0) {
1054           if(unprintableCodes == null) {
1055             unprintableCodes = new LinkedList<ExtraCodes>();
1056           }
1057           
1058           // keep track of the extra codes for later
1059           unprintableCodes.add(new ExtraCodes(charOffset, bytes));
1060         }
1061 
1062         // note, we do _not_ increment the charOffset for unprintable chars
1063         continue;
1064       }
1065 
1066       InternationalCodes inatCodes = INTERNATIONAL_CODES.get(cKey);
1067       if(inatCodes != null) {
1068 
1069         // we write the "inline" portion of the international codes
1070         // immediately, and queue the extra codes for later
1071         tmpBout.write(inatCodes._inlineCodes);
1072 
1073         if(internationalCodes == null) {
1074           internationalCodes = new LinkedList<ExtraCodes>();
1075         }
1076 
1077         // keep track of the extra codes for later
1078         internationalCodes.add(new ExtraCodes(charOffset,
1079                                               inatCodes._extraCodes));
1080 
1081         ++charOffset;
1082         continue;
1083       }
1084 
1085       // bummer, out of luck
1086       throw new IOException("unmapped string index value " + c);
1087     }
1088 
1089     // write end text flag
1090     tmpBout.write(END_TEXT);
1091 
1092     boolean hasExtraText = ((unprintableCodes != null) ||
1093                             (internationalCodes != null));
1094     if(hasExtraText) {
1095 
1096       // we write all the international extra bytes first
1097       if(internationalCodes != null) {
1098 
1099         // we write a placeholder char for each non-international char before
1100         // the extra chars for the international char
1101         charOffset = 0;
1102         Iterator<ExtraCodes> iter = internationalCodes.iterator();
1103         while(iter.hasNext()) {
1104           ExtraCodes extraCodes = iter.next();
1105           while(charOffset < extraCodes._charOffset) {
1106             tmpBout.write(INTERNATIONAL_EXTRA_PLACEHOLDER);
1107             ++charOffset;
1108           }
1109           tmpBout.write(extraCodes._extraCodes);
1110           ++charOffset;
1111         }
1112       }
1113 
1114       // then we write all the unprintable extra bytes
1115       if(unprintableCodes != null) {
1116 
1117         // write a single prefix for all unprintable chars
1118         tmpBout.write(UNPRINTABLE_COMMON_PREFIX);
1119         
1120         // we write a whacky combo of bytes for each unprintable char which
1121         // includes a funky offset and extra char itself
1122         Iterator<ExtraCodes> iter = unprintableCodes.iterator();
1123         while(iter.hasNext()) {
1124           ExtraCodes extraCodes = iter.next();
1125           int offset =
1126             (UNPRINTABLE_COUNT_START +
1127              (UNPRINTABLE_COUNT_MULTIPLIER * extraCodes._charOffset))
1128             | UNPRINTABLE_OFFSET_FLAGS;
1129 
1130           // write offset as big-endian short
1131           tmpBout.write((offset >> 8) & 0xFF);
1132           tmpBout.write(offset & 0xFF);
1133           
1134           tmpBout.write(UNPRINTABLE_MIDFIX);
1135           tmpBout.write(extraCodes._extraCodes);
1136         }
1137       }
1138 
1139     }
1140 
1141     // handle descending order by inverting the bytes
1142     if(!isAscending) {
1143 
1144       // we actually write the end byte before flipping the bytes, and write
1145       // another one after flipping
1146       tmpBout.write(END_EXTRA_TEXT);
1147       
1148       // we actually wrote into a temporary array so that we can invert the
1149       // bytes before writing them to the final array
1150       bout.write(flipBytes(tmpBout.toByteArray()));
1151 
1152     }
1153 
1154     // write end extra text
1155     bout.write(END_EXTRA_TEXT);    
1156   }
1157 
1158   /**
1159    * Creates one of the special index entries.
1160    */
1161   private static Entry createSpecialEntry(RowId rowId) {
1162     return new Entry((byte[])null, rowId);
1163   }
1164 
1165   /**
1166    * Constructs a ColumnDescriptor of the relevant type for the given Column.
1167    */
1168   private ColumnDescriptor newColumnDescriptor(Column col, byte flags)
1169     throws IOException
1170   {
1171     switch(col.getType()) {
1172     case TEXT:
1173     case MEMO:
1174       return new TextColumnDescriptor(col, flags);
1175     case INT:
1176     case LONG:
1177     case MONEY:
1178       return new IntegerColumnDescriptor(col, flags);
1179     case FLOAT:
1180     case DOUBLE:
1181     case SHORT_DATE_TIME:
1182       return new FloatingPointColumnDescriptor(col, flags);
1183     case NUMERIC:
1184       return new FixedPointColumnDescriptor(col, flags);
1185     case BYTE:
1186       return new ByteColumnDescriptor(col, flags);
1187     case BOOLEAN:
1188       return new BooleanColumnDescriptor(col, flags);
1189 
1190     default:
1191       // FIXME we can't modify this index at this point in time
1192       setReadOnly();
1193       return new ReadOnlyColumnDescriptor(col, flags);
1194     }
1195   }
1196 
1197   /**
1198    * Returns the EntryType based on the given entry info.
1199    */
1200   private static EntryType determineEntryType(byte[] entryBytes, RowId rowId)
1201   {
1202     if(entryBytes != null) {
1203       return ((rowId.getType() == RowId.Type.NORMAL) ?
1204               EntryType.NORMAL :
1205               ((rowId.getType() == RowId.Type.ALWAYS_FIRST) ?
1206                EntryType.FIRST_VALID : EntryType.LAST_VALID));
1207     } else if(!rowId.isValid()) {
1208       // this is a "special" entry (first/last)
1209       return ((rowId.getType() == RowId.Type.ALWAYS_FIRST) ?
1210               EntryType.ALWAYS_FIRST : EntryType.ALWAYS_LAST);
1211     }
1212     throw new IllegalArgumentException("Values was null for valid entry");
1213   }
1214 
1215   /**
1216    * Returns the maximum amount of entry data which can be encoded on any
1217    * index page.
1218    */
1219   private static int calcMaxPageEntrySize(JetFormat format)
1220   {
1221     // the max data we can fit on a page is the min of the space on the page
1222     // vs the number of bytes which can be encoded in the entry mask
1223     int pageDataSize = (format.PAGE_SIZE -
1224                         (format.OFFSET_INDEX_ENTRY_MASK +
1225                          format.SIZE_INDEX_ENTRY_MASK));
1226     int entryMaskSize = (format.SIZE_INDEX_ENTRY_MASK * 8);
1227     return Math.min(pageDataSize, entryMaskSize);
1228   }
1229   
1230   /**
1231    * Information about the columns in an index.  Also encodes new index
1232    * values.
1233    */
1234   public static abstract class ColumnDescriptor
1235   {
1236     private final Column _column;
1237     private final byte _flags;
1238 
1239     private ColumnDescriptor(Column column, byte flags)
1240       throws IOException
1241     {
1242       _column = column;
1243       _flags = flags;
1244     }
1245 
1246     public Column getColumn() {
1247       return _column;
1248     }
1249 
1250     public byte getFlags() {
1251       return _flags;
1252     }
1253 
1254     public boolean isAscending() {
1255       return((getFlags() & ASCENDING_COLUMN_FLAG) != 0);
1256     }
1257     
1258     public int getColumnIndex() {
1259       return getColumn().getColumnIndex();
1260     }
1261     
1262     public String getName() {
1263       return getColumn().getName();
1264     }
1265 
1266     protected boolean isNullValue(Object value) {
1267       return (value == null);
1268     }
1269     
1270     protected final void writeValue(Object value, ByteArrayOutputStream bout)
1271       throws IOException
1272     {
1273       if(isNullValue(value)) {
1274         // write null value
1275         bout.write(getNullEntryFlag(isAscending()));
1276         return;
1277       }
1278       
1279       // write the start flag
1280       bout.write(getStartEntryFlag(isAscending()));
1281       // write the rest of the value
1282       writeNonNullValue(value, bout);
1283     }
1284 
1285     protected abstract void writeNonNullValue(
1286         Object value, ByteArrayOutputStream bout)
1287       throws IOException; 
1288     
1289     @Override
1290     public String toString() {
1291       return "ColumnDescriptor " + getColumn() + "\nflags: " + getFlags();
1292     }
1293   }
1294 
1295   /**
1296    * ColumnDescriptor for integer based columns.
1297    */
1298   private static final class IntegerColumnDescriptor extends ColumnDescriptor
1299   {
1300     private IntegerColumnDescriptor(Column column, byte flags)
1301       throws IOException
1302     {
1303       super(column, flags);
1304     }
1305     
1306     @Override
1307     protected void writeNonNullValue(
1308         Object value, ByteArrayOutputStream bout)
1309       throws IOException
1310     {
1311       byte[] valueBytes = encodeNumberColumnValue(value, getColumn());
1312       
1313       // bit twiddling rules:
1314       // - isAsc  => flipFirstBit
1315       // - !isAsc => flipFirstBit, flipBytes
1316       
1317       flipFirstBitInByte(valueBytes, 0);
1318       if(!isAscending()) {
1319         flipBytes(valueBytes);
1320       }
1321       
1322       bout.write(valueBytes);
1323     }    
1324   }
1325   
1326   /**
1327    *