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.util.Collection;
21  import java.util.HashSet;
22  import java.util.Iterator;
23  import java.util.Map;
24  import java.util.Set;
25  
26  import com.healthmarketscience.jackcess.Index;
27  import com.healthmarketscience.jackcess.IndexCursor;
28  import com.healthmarketscience.jackcess.Row;
29  import com.healthmarketscience.jackcess.RuntimeIOException;
30  import com.healthmarketscience.jackcess.impl.TableImpl.RowState;
31  import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher;
32  import com.healthmarketscience.jackcess.util.ColumnMatcher;
33  import com.healthmarketscience.jackcess.util.EntryIterableBuilder;
34  import com.healthmarketscience.jackcess.util.SimpleColumnMatcher;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
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 Log LOG = LogFactory.getLog(IndexCursorImpl.class);
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().getUnsupportedReason() != null) {
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<String>();
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.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.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     // either we found a row with the given value, or none exist in the table
264     return currentRowMatchesImpl(columnPattern, valuePattern, columnMatcher);
265   }
266 
267   /**
268    * Moves to the first row (as defined by the cursor) where the index entries
269    * match the given values.  Caller manages save/restore on failure.
270    *
271    * @param rowValues the column values built from the index column values
272    * @param requireMatch whether or not an exact match is desired
273    * @return {@code true} if a valid row was found with the given values,
274    *         {@code false} if no row was found
275    */
276   protected boolean findFirstRowByEntryImpl(Object[] rowValues,
277                                             boolean requireMatch,
278                                             ColumnMatcher columnMatcher)
279     throws IOException
280   {
281     if(!findPotentialRow(rowValues, requireMatch)) {
282       return false;
283     } else if(!requireMatch) {
284       // nothing more to do, we have moved to the closest row
285       return true;
286     }
287 
288     return currentRowMatchesEntryImpl(rowValues, columnMatcher);
289   }
290 
291   @Override
292   protected boolean findAnotherRowImpl(
293       Map<String,?> rowPattern, boolean moveForward,
294       ColumnMatcher columnMatcher, Object searchInfo)
295     throws IOException
296   {
297     Object[] rowValues = (Object[])searchInfo;
298 
299     if((rowValues == null) || !isAtBeginning(moveForward)) {
300       // use the default table scan if we don't have index data or we are
301       // mid-cursor
302       return super.findAnotherRowImpl(rowPattern, moveForward, columnMatcher,
303                                       rowValues);
304     }
305 
306     // sweet, we can use our index
307     if(!findPotentialRow(rowValues, true)) {
308       // at end of index, no potential matches
309       return false;
310     }
311 
312     // determine if the pattern columns exactly match the index columns
313     boolean exactColumnMatch = rowPattern.keySet().equals(
314         getIndexEntryPattern());
315 
316     // there may be multiple rows which fit the pattern subset used by
317     // the index, so we need to keep checking until our index values no
318     // longer match
319     do {
320 
321       if(!currentRowMatchesEntryImpl(rowValues, columnMatcher)) {
322         // there are no more rows which could possibly match
323         break;
324       }
325 
326       // note, if exactColumnMatch, no need to do an extra comparison with the
327       // current row (since the entry match check above is equivalent to this
328       // check)
329       if(exactColumnMatch || currentRowMatchesImpl(rowPattern, columnMatcher)) {
330         // found it!
331         return true;
332       }
333 
334     } while(moveToAnotherRow(moveForward));
335 
336     // none of the potential rows matched
337     return false;
338   }
339 
340   private boolean currentRowMatchesEntryImpl(Object[] rowValues,
341                                              ColumnMatcher columnMatcher)
342     throws IOException
343   {
344     // check the next row to see if it actually matches
345     Row row = getCurrentRow(getIndexEntryPattern());
346 
347     for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
348 
349       Object patValue = rowValues[col.getColumnIndex()];
350 
351       if((patValue == IndexData.MIN_VALUE) ||
352          (patValue == IndexData.MAX_VALUE)) {
353         // all remaining entry values are "special" (used for partial lookups)
354         return true;
355       }
356 
357       String columnName = col.getName();
358       Object rowValue = row.get(columnName);
359       if(!columnMatcher.matches(getTable(), columnName, patValue, rowValue)) {
360         return false;
361       }
362     }
363 
364     return true;
365   }
366 
367   private boolean findPotentialRow(Object[] rowValues, boolean requireMatch)
368     throws IOException
369   {
370     _entryCursor.beforeEntry(rowValues);
371     IndexData.Entry startEntry = _entryCursor.getNextEntry();
372     if(requireMatch && !startEntry.getRowId().isValid()) {
373       // at end of index, no potential matches
374       return false;
375     }
376     // move to position and check it out
377     restorePosition(new IndexPosition(startEntry));
378     return true;
379   }
380 
381   @Override
382   protected Object prepareSearchInfo(ColumnImpl columnPattern, Object valuePattern)
383   {
384     // attempt to generate a lookup row for this index
385     return _entryCursor.getIndexData().constructPartialIndexRow(
386         IndexData.MIN_VALUE, columnPattern.getName(), valuePattern);
387   }
388 
389   @Override
390   protected Object prepareSearchInfo(Map<String,?> rowPattern)
391   {
392     // attempt to generate a lookup row for this index
393     return _entryCursor.getIndexData().constructPartialIndexRow(
394         IndexData.MIN_VALUE, rowPattern);
395   }
396 
397   @Override
398   protected boolean keepSearching(ColumnMatcher columnMatcher,
399                                   Object searchInfo)
400     throws IOException
401   {
402     if(searchInfo instanceof Object[]) {
403       // if we have a lookup row for this index, then we only need to continue
404       // searching while we are looking at rows which match the index lookup
405       // value(s).  once we move past those rows, no other rows could possibly
406       // match.
407       return currentRowMatchesEntryImpl((Object[])searchInfo, columnMatcher);
408     }
409     // we are doing a full table scan
410     return true;
411   }
412 
413   private Object[] toRowValues(Object[] entryValues)
414   {
415     return _entryCursor.getIndexData().constructPartialIndexRowFromEntry(
416         IndexData.MIN_VALUE, entryValues);
417   }
418 
419   @Override
420   protected PositionImpl findAnotherPosition(
421       RowState rowState, PositionImpl curPos, boolean moveForward)
422     throws IOException
423   {
424     IndexDirHandler handler = getDirHandler(moveForward);
425     IndexPosition endPos = (IndexPosition)handler.getEndPosition();
426     IndexData.Entry entry = handler.getAnotherEntry();
427     return ((!entry.equals(endPos.getEntry())) ?
428             new IndexPosition(entry) : endPos);
429   }
430 
431   @Override
432   protected ColumnMatcher getDefaultColumnMatcher() {
433     if(getIndex().isUnique()) {
434       // text indexes are case-insensitive, therefore we should always use a
435       // case-insensitive matcher for unique indexes.
436       return CaseInsensitiveColumnMatcher.INSTANCE;
437     }
438     return SimpleColumnMatcher.INSTANCE;
439   }
440 
441   /**
442    * Handles moving the table index cursor in a given direction.  Separates
443    * cursor logic from value storage.
444    */
445   private abstract class IndexDirHandler extends DirHandler {
446     public abstract IndexData.Entry getAnotherEntry()
447       throws IOException;
448   }
449 
450   /**
451    * Handles moving the table index cursor forward.
452    */
453   private final class ForwardIndexDirHandler extends IndexDirHandler {
454     @Override
455     public PositionImpl getBeginningPosition() {
456       return getFirstPosition();
457     }
458     @Override
459     public PositionImpl getEndPosition() {
460       return getLastPosition();
461     }
462     @Override
463     public IndexData.Entry getAnotherEntry() throws IOException {
464       return _entryCursor.getNextEntry();
465     }
466   }
467 
468   /**
469    * Handles moving the table index cursor backward.
470    */
471   private final class ReverseIndexDirHandler extends IndexDirHandler {
472     @Override
473     public PositionImpl getBeginningPosition() {
474       return getLastPosition();
475     }
476     @Override
477     public PositionImpl getEndPosition() {
478       return getFirstPosition();
479     }
480     @Override
481     public IndexData.Entry getAnotherEntry() throws IOException {
482       return _entryCursor.getPreviousEntry();
483     }
484   }
485 
486   /**
487    * Value object which maintains the current position of an IndexCursor.
488    */
489   private static final class IndexPosition extends PositionImpl
490   {
491     private final IndexData.Entry _entry;
492 
493     private IndexPosition(IndexData.Entry entry) {
494       _entry = entry;
495     }
496 
497     @Override
498     public RowIdImpl getRowId() {
499       return getEntry().getRowId();
500     }
501 
502     public IndexData.Entry getEntry() {
503       return _entry;
504     }
505 
506     @Override
507     protected boolean equalsImpl(Object o) {
508       return getEntry().equals(((IndexPosition)o).getEntry());
509     }
510 
511     @Override
512     public String toString() {
513       return "Entry = " + getEntry();
514     }
515   }
516 
517   /**
518    * Row iterator (by matching entry) for this cursor, modifiable.
519    */
520   private final class EntryIterator extends BaseIterator
521   {
522     private final Object[] _rowValues;
523 
524     private EntryIterator(Collection<String> columnNames, Object[] rowValues,
525                           ColumnMatcher columnMatcher)
526     {
527       super(columnNames, false, MOVE_FORWARD, columnMatcher);
528       _rowValues = rowValues;
529       try {
530         _hasNext = findFirstRowByEntryImpl(rowValues, true, _columnMatcher);
531         _validRow = _hasNext;
532       } catch(IOException e) {
533           throw new RuntimeIOException(e);
534       }
535     }
536 
537     @Override
538     protected boolean findNext() throws IOException {
539       return (moveToNextRow() &&
540               currentRowMatchesEntryImpl(_rowValues, _colMatcher));
541     }
542   }
543 
544 }