View Javadoc
1   /*
2   Copyright (c) 2011 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.IOException;
20  import java.io.UncheckedIOException;
21  import java.lang.System.Logger;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import com.healthmarketscience.jackcess.Index;
30  import com.healthmarketscience.jackcess.IndexCursor;
31  import com.healthmarketscience.jackcess.Row;
32  import com.healthmarketscience.jackcess.impl.TableImpl.RowState;
33  import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher;
34  import com.healthmarketscience.jackcess.util.ColumnMatcher;
35  import com.healthmarketscience.jackcess.util.EntryIterableBuilder;
36  import com.healthmarketscience.jackcess.util.SimpleColumnMatcher;
37  
38  /**
39   * Cursor backed by an index with extended traversal options.
40   *
41   * @author James Ahlborn
42   */
43  public class IndexCursorImpl extends CursorImpl implements IndexCursor
44  {
45    private static final Logger LOG = System.getLogger(IndexCursorImpl.class.getName());
46  
47    /** IndexDirHandler for forward traversal */
48    private final IndexDirHandler _forwardDirHandler =
49      new ForwardIndexDirHandler();
50    /** IndexDirHandler for backward traversal */
51    private final IndexDirHandler _reverseDirHandler =
52      new ReverseIndexDirHandler();
53    /** logical index which this cursor is using */
54    private final IndexImpl _index;
55    /** Cursor over the entries of the relevant index */
56    private final IndexData.EntryCursor _entryCursor;
57    /** column names for the index entry columns */
58    private Set<String> _indexEntryPattern;
59  
60    private IndexCursorImpl(TableImpl table, IndexImpl index,
61                            IndexData.EntryCursor entryCursor)
62      throws IOException
63    {
64      super(new IdImpl(table, index), table,
65            new IndexPosition(entryCursor.getFirstEntry()),
66            new IndexPosition(entryCursor.getLastEntry()));
67      _index = index;
68      _index.initialize();
69      _entryCursor = entryCursor;
70    }
71  
72    /**
73     * Creates an indexed cursor for the given table, narrowed to the given
74     * range.
75     * <p>
76     * Note, index based table traversal may not include all rows, as certain
77     * types of indexes do not include all entries (namely, some indexes ignore
78     * null entries, see {@link Index#shouldIgnoreNulls}).
79     *
80     * @param table the table over which this cursor will traverse
81     * @param index index for the table which will define traversal order as
82     *              well as enhance certain lookups
83     * @param startRow the first row of data for the cursor, or {@code null} for
84     *                 the first entry
85     * @param startInclusive whether or not startRow is inclusive or exclusive
86     * @param endRow the last row of data for the cursor, or {@code null} for
87     *               the last entry
88     * @param endInclusive whether or not endRow is inclusive or exclusive
89     */
90    public static IndexCursorImpl createCursor(TableImpl table, IndexImpl index,
91                                               Object[] startRow,
92                                               boolean startInclusive,
93                                               Object[] endRow,
94                                               boolean endInclusive)
95      throws IOException
96    {
97      if(table != index.getTable()) {
98        throw new IllegalArgumentException(
99            "Given index is not for given table: " + index + ", " + table);
100     }
101     if(!index.getIndexData().isValid()) {
102       throw new IllegalArgumentException(
103           "Given index " + index +
104           " is not usable for indexed lookups due to " +
105           index.getIndexData().getUnsupportedReason());
106     }
107     IndexCursorImpl/IndexCursorImpl.html#IndexCursorImpl">IndexCursorImpl cursor = new IndexCursorImpl(
108         table, index, index.cursor(startRow, startInclusive,
109                                    endRow, endInclusive));
110     // init the column matcher appropriately for the index type
111     cursor.setColumnMatcher(null);
112     return cursor;
113   }
114 
115   private Set<String> getIndexEntryPattern()
116   {
117     if(_indexEntryPattern == null) {
118       // init our set of index column names
119       _indexEntryPattern = new HashSet<>();
120       for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
121         _indexEntryPattern.add(col.getName());
122       }
123     }
124     return _indexEntryPattern;
125   }
126 
127   @Override
128   public IndexImpl getIndex() {
129     return _index;
130   }
131 
132   @Override
133   public Row findRowByEntry(Object... entryValues)
134     throws IOException
135   {
136     if(findFirstRowByEntry(entryValues)) {
137       return getCurrentRow();
138     }
139     return null;
140   }
141 
142   @Override
143   public boolean findFirstRowByEntry(Object... entryValues)
144     throws IOException
145   {
146     PositionImpl curPos = _curPos;
147     PositionImpl prevPos = _prevPos;
148     boolean found = false;
149     try {
150       found = findFirstRowByEntryImpl(toRowValues(entryValues), true,
151                                       _columnMatcher);
152       return found;
153     } finally {
154       if(!found) {
155         try {
156           restorePosition(curPos, prevPos);
157         } catch(IOException e) {
158           LOG.log(Logger.Level.ERROR, "Failed restoring position", e);
159         }
160       }
161     }
162   }
163 
164   @Override
165   public void findClosestRowByEntry(Object... entryValues)
166     throws IOException
167   {
168     PositionImpl curPos = _curPos;
169     PositionImpl prevPos = _prevPos;
170     boolean found = false;
171     try {
172       findFirstRowByEntryImpl(toRowValues(entryValues), false,
173                               _columnMatcher);
174       found = true;
175     } finally {
176       if(!found) {
177         try {
178           restorePosition(curPos, prevPos);
179         } catch(IOException e) {
180           LOG.log(Logger.Level.ERROR, "Failed restoring position", e);
181         }
182       }
183     }
184   }
185 
186   @Override
187   public boolean currentRowMatchesEntry(Object... entryValues)
188     throws IOException
189   {
190     return currentRowMatchesEntryImpl(toRowValues(entryValues), _columnMatcher);
191   }
192 
193   @Override
194   public EntryIterableBuilder newEntryIterable(Object... entryValues) {
195     return new EntryIterableBuilder(this, entryValues);
196   }
197 
198   public Iterator<Row> entryIterator(EntryIterableBuilder iterBuilder) {
199     return new EntryIterator(iterBuilder.getColumnNames(),
200                              toRowValues(iterBuilder.getEntryValues()),
201                              iterBuilder.getColumnMatcher());
202   }
203 
204   @Override
205   protected IndexDirHandler getDirHandler(boolean moveForward) {
206     return (moveForward ? _forwardDirHandler : _reverseDirHandler);
207   }
208 
209   @Override
210   protected boolean isUpToDate() {
211     return(super.isUpToDate() && _entryCursor.isUpToDate());
212   }
213 
214   @Override
215   protected void reset(boolean moveForward) {
216     _entryCursor.reset(moveForward);
217     super.reset(moveForward);
218   }
219 
220   @Override
221   protected void restorePositionImpl(PositionImpl curPos, PositionImpl prevPos)
222     throws IOException
223   {
224     if(!(curPos instanceof IndexPosition) ||
225        !(prevPos instanceof IndexPosition)) {
226       throw new IllegalArgumentException(
227           "Restored positions must be index positions");
228     }
229     _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(),
230                                  ((IndexPosition)prevPos).getEntry());
231     super.restorePositionImpl(curPos, prevPos);
232   }
233 
234   @Override
235   protected PositionImpl getRowPosition(RowIdImpl rowId) throws IOException
236   {
237     // we need to get the index entry which corresponds with this row
238     Row row = getTable().getRow(getRowState(), rowId, getIndexEntryPattern());
239     _entryCursor.beforeEntry(getTable().asRow(row));
240     return new IndexPosition(_entryCursor.getNextEntry());
241   }
242 
243   @Override
244   protected boolean findAnotherRowImpl(
245       ColumnImpl columnPattern, Object valuePattern, boolean moveForward,
246       ColumnMatcher columnMatcher, Object searchInfo)
247     throws IOException
248   {
249     Object[] rowValues = (Object[])searchInfo;
250 
251     if((rowValues == null) || !isAtBeginning(moveForward)) {
252       // use the default table scan if we don't have index data or we are
253       // mid-cursor
254       return super.findAnotherRowImpl(columnPattern, valuePattern, moveForward,
255                                       columnMatcher, rowValues);
256     }
257 
258     // sweet, we can use our index
259     if(!findPotentialRow(rowValues, true)) {
260       return false;
261     }
262 
263     return findMatchingRowInRun(
264         rowValues, moveForward, columnMatcher,
265         () -> currentRowMatchesImpl(columnPattern, valuePattern,
266                                     columnMatcher));
267   }
268 
269   /**
270    * Moves to the first row (as defined by the cursor) where the index entries
271    * match the given values.  Caller manages save/restore on failure.
272    *
273    * @param rowValues the column values built from the index column values
274    * @param requireMatch whether or not an exact match is desired
275    * @return {@code true} if a valid row was found with the given values,
276    *         {@code false} if no row was found
277    */
278   protected boolean findFirstRowByEntryImpl(Object[] rowValues,
279                                             boolean requireMatch,
280                                             ColumnMatcher columnMatcher)
281     throws IOException
282   {
283     if(!findPotentialRow(rowValues, requireMatch)) {
284       return false;
285     } else if(!requireMatch) {
286       // nothing more to do, we have moved to the closest row
287       return true;
288     }
289 
290     return findMatchingRowInRun(rowValues, MOVE_FORWARD, columnMatcher,
291                                 MATCHES_ENTRY);
292   }
293 
294   @Override
295   protected boolean findAnotherRowImpl(
296       Map<String,?> rowPattern, boolean moveForward,
297       ColumnMatcher columnMatcher, Object searchInfo)
298     throws IOException
299   {
300     Object[] rowValues = (Object[])searchInfo;
301 
302     if((rowValues == null) || !isAtBeginning(moveForward)) {
303       // use the default table scan if we don't have index data or we are
304       // mid-cursor
305       return super.findAnotherRowImpl(rowPattern, moveForward, columnMatcher,
306                                       rowValues);
307     }
308 
309     // sweet, we can use our index
310     if(!findPotentialRow(rowValues, true)) {
311       // at end of index, no potential matches
312       return false;
313     }
314 
315     // determine if the pattern columns exactly match the index columns
316     boolean exactColumnMatch = rowPattern.keySet().equals(
317         getIndexEntryPattern());
318 
319     // note, if exactColumnMatch, no need to do an extra comparison with the
320     // current row (since the entry match check in the run is equivalent to
321     // this check)
322     return findMatchingRowInRun(
323         rowValues, moveForward, columnMatcher,
324         () -> (exactColumnMatch ||
325                currentRowMatchesImpl(rowPattern, columnMatcher)));
326   }
327 
328   /**
329    * A test of the row the cursor is on, run over the rows which could match a
330    * search.
331    */
332   private interface RowTest
333   {
334     public boolean matches() throws IOException;
335   }
336 
337   /** the test for a search which asks nothing beyond the index entry */
338   private static final RowTest MATCHES_ENTRY = () -> true;
339 
340   /**
341    * Returns whether any of the rows which could still match the search passes
342    * the given test, leaving the cursor on the first which does.
343    * <p>
344    * The cursor starts on the row the search landed on, which is the first of
345    * the rows sharing its index entry.
346    */
347   private boolean findMatchingRowInRun(Object[] rowValues, boolean moveForward,
348                                        ColumnMatcher columnMatcher,
349                                        RowTest test)
350     throws IOException
351   {
352     byte[] runBytes = currentEntryBytes();
353 
354     do {
355 
356       if(currentRowMatchesEntryImpl(rowValues, columnMatcher) &&
357          test.matches()) {
358         return true;
359       }
360 
361     } while(moveToAnotherRow(moveForward) &&
362             couldStillMatch(rowValues, columnMatcher, runBytes));
363 
364     return false;
365   }
366 
367   /**
368    * Returns whether the row the cursor is on can still match the search.
369    */
370   private boolean couldStillMatch(Object[] rowValues,
371                                   ColumnMatcher columnMatcher,
372                                   byte[] runBytes)
373     throws IOException
374   {
375     if(currentRowMatchesEntryImpl(rowValues, columnMatcher)) {
376       return true;
377     }
378 
379     // the values differ, but one index entry can hold several rows, since the
380     // entry of a text value folds case, trailing spaces and the characters
381     // the collation treats alike.  the rest of that run is still worth
382     // reading, and a row past it has a greater entry and so cannot hold the
383     // values searched for
384     byte[] entryBytes = currentEntryBytes();
385     return ((runBytes != null) && Arrays.equals(runBytes, entryBytes));
386   }
387 
388   /**
389    * Returns the entry bytes of the row the cursor is on, or {@code null} if
390    * it is not on a row.
391    */
392   private byte[] currentEntryBytes() {
393     IndexData.Entry entry = ((IndexPosition)_curPos).getEntry();
394     return (entry.isValid() ? entry.getEntryBytes() : null);
395   }
396 
397   private boolean currentRowMatchesEntryImpl(Object[] rowValues,
398                                              ColumnMatcher columnMatcher)
399     throws IOException
400   {
401     // check the next row to see if it actually matches
402     Row row = getCurrentRow(getIndexEntryPattern());
403 
404     for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
405 
406       Object patValue = rowValues[col.getColumnIndex()];
407 
408       if((patValue == IndexData.MIN_VALUE) ||
409          (patValue == IndexData.MAX_VALUE)) {
410         // all remaining entry values are "special" (used for partial lookups)
411         return true;
412       }
413 
414       String columnName = col.getName();
415       Object rowValue = row.get(columnName);
416       if(!columnMatcher.matches(getTable(), columnName, patValue, rowValue)) {
417         return false;
418       }
419     }
420 
421     return true;
422   }
423 
424   private boolean findPotentialRow(Object[] rowValues, boolean requireMatch)
425     throws IOException
426   {
427     _entryCursor.beforeEntry(rowValues);
428     IndexData.Entry startEntry = _entryCursor.getNextEntry();
429     if(requireMatch && !startEntry.getRowId().isValid()) {
430       // at end of index, no potential matches
431       return false;
432     }
433     // move to position and check it out
434     restorePosition(new IndexPosition(startEntry));
435     return true;
436   }
437 
438   @Override
439   protected Object prepareSearchInfo(ColumnImpl columnPattern, Object valuePattern)
440   {
441     // attempt to generate a lookup row for this index
442     return _entryCursor.getIndexData().constructPartialIndexRow(
443         IndexData.MIN_VALUE, columnPattern.getName(), valuePattern);
444   }
445 
446   @Override
447   protected Object prepareSearchInfo(Map<String,?> rowPattern)
448   {
449     // attempt to generate a lookup row for this index
450     return _entryCursor.getIndexData().constructPartialIndexRow(
451         IndexData.MIN_VALUE, rowPattern);
452   }
453 
454   @Override
455   protected boolean keepSearching(ColumnMatcher columnMatcher,
456                                   Object searchInfo)
457     throws IOException
458   {
459     if(searchInfo instanceof Object[]) {
460       // if we have a lookup row for this index, then we only need to continue
461       // searching while we are looking at rows which match the index lookup
462       // value(s).  once we move past those rows, no other rows could possibly
463       // match.
464       return currentRowMatchesEntryImpl((Object[])searchInfo, columnMatcher);
465     }
466     // we are doing a full table scan
467     return true;
468   }
469 
470   private Object[] toRowValues(Object[] entryValues)
471   {
472     return _entryCursor.getIndexData().constructPartialIndexRowFromEntry(
473         IndexData.MIN_VALUE, entryValues);
474   }
475 
476   @Override
477   protected PositionImpl findAnotherPosition(
478       RowState rowState, PositionImpl curPos, boolean moveForward)
479     throws IOException
480   {
481     IndexDirHandler handler = getDirHandler(moveForward);
482     IndexPosition endPos = (IndexPosition)handler.getEndPosition();
483     IndexData.Entry entry = handler.getAnotherEntry();
484     return ((!entry.equals(endPos.getEntry())) ?
485             new IndexPosition(entry) : endPos);
486   }
487 
488   @Override
489   protected ColumnMatcher getDefaultColumnMatcher() {
490     if(getIndex().isUnique()) {
491       // text indexes are case-insensitive, therefore we should always use a
492       // case-insensitive matcher for unique indexes.
493       return CaseInsensitiveColumnMatcher.INSTANCE;
494     }
495     return SimpleColumnMatcher.INSTANCE;
496   }
497 
498   /**
499    * Handles moving the table index cursor in a given direction.  Separates
500    * cursor logic from value storage.
501    */
502   private abstract class IndexDirHandler extends DirHandler {
503     public abstract IndexData.Entry getAnotherEntry()
504       throws IOException;
505   }
506 
507   /**
508    * Handles moving the table index cursor forward.
509    */
510   private final class ForwardIndexDirHandler extends IndexDirHandler {
511     @Override
512     public PositionImpl getBeginningPosition() {
513       return getFirstPosition();
514     }
515     @Override
516     public PositionImpl getEndPosition() {
517       return getLastPosition();
518     }
519     @Override
520     public IndexData.Entry getAnotherEntry() throws IOException {
521       return _entryCursor.getNextEntry();
522     }
523   }
524 
525   /**
526    * Handles moving the table index cursor backward.
527    */
528   private final class ReverseIndexDirHandler extends IndexDirHandler {
529     @Override
530     public PositionImpl getBeginningPosition() {
531       return getLastPosition();
532     }
533     @Override
534     public PositionImpl getEndPosition() {
535       return getFirstPosition();
536     }
537     @Override
538     public IndexData.Entry getAnotherEntry() throws IOException {
539       return _entryCursor.getPreviousEntry();
540     }
541   }
542 
543   /**
544    * Value object which maintains the current position of an IndexCursor.
545    */
546   private static final class IndexPosition extends PositionImpl
547   {
548     private final IndexData.Entry _entry;
549 
550     private IndexPosition(IndexData.Entry entry) {
551       _entry = entry;
552     }
553 
554     @Override
555     public RowIdImpl getRowId() {
556       return getEntry().getRowId();
557     }
558 
559     public IndexData.Entry getEntry() {
560       return _entry;
561     }
562 
563     @Override
564     protected boolean equalsImpl(Object o) {
565       return getEntry().equals(((IndexPosition)o).getEntry());
566     }
567 
568     @Override
569     public String toString() {
570       return "Entry = " + getEntry();
571     }
572   }
573 
574   /**
575    * Row iterator (by matching entry) for this cursor, modifiable.
576    */
577   private final class EntryIterator extends BaseIterator
578   {
579     private final Object[] _rowValues;
580 
581     private EntryIterator(Collection<String> columnNames, Object[] rowValues,
582                           ColumnMatcher columnMatcher)
583     {
584       super(columnNames, false, MOVE_FORWARD, columnMatcher);
585       _rowValues = rowValues;
586       try {
587         _hasNext = findFirstRowByEntryImpl(rowValues, true, _columnMatcher);
588         _validRow = _hasNext;
589       } catch(IOException e) {
590           throw new UncheckedIOException(e);
591       }
592     }
593 
594     @Override
595     protected boolean findNext() throws IOException {
596       // the next row may share its entry with one which does not match, so
597       // the rest of that run is read before the iteration ends
598       return (moveToNextRow() &&
599               findMatchingRowInRun(_rowValues, MOVE_FORWARD, _colMatcher,
600                                    MATCHES_ENTRY));
601     }
602   }
603 
604 }