View Javadoc
1   /*
2   Copyright (c) 2007 Health Market Science, Inc.
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;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.List;
23  import java.util.Map;
24  
25  import com.healthmarketscience.jackcess.impl.CursorImpl;
26  import com.healthmarketscience.jackcess.impl.IndexCursorImpl;
27  import com.healthmarketscience.jackcess.impl.IndexData;
28  import com.healthmarketscience.jackcess.impl.IndexImpl;
29  import com.healthmarketscience.jackcess.impl.TableImpl;
30  import com.healthmarketscience.jackcess.util.ColumnMatcher;
31  
32  
33  /**
34   * Builder style class for constructing a {@link Cursor}.  By default, a
35   * cursor is created at the beginning of the table, and any start/end rows are
36   * inclusive.
37   * <p/>
38   * Simple example traversal:
39   * <pre>
40   *   for(Row row : table.newCursor().toCursor()) {
41   *     // ... process each row ...
42   *   }
43   * </pre>
44   * <p/>
45   * Simple example search:
46   * <pre>
47   *   Row row = CursorBuilder.findRow(table, Collections.singletonMap(col, "foo"));
48   * </pre>
49   *
50   * @author James Ahlborn
51   * @usage _general_class_
52   */
53  public class CursorBuilder {
54    /** the table which the cursor will traverse */
55    private final TableImpl _table;
56    /** optional index to use in traversal */
57    private IndexImpl _index;
58    /** optional start row for an index cursor */
59    private Object[] _startRow;
60    /** whether or not start row for an index cursor is inclusive */
61    private boolean _startRowInclusive = true;
62    /** optional end row for an index cursor */
63    private Object[] _endRow;
64    /** whether or not end row for an index cursor is inclusive */
65    private boolean _endRowInclusive = true;
66    /** whether to start at beginning or end of cursor */
67    private boolean _beforeFirst = true;
68    /** optional save point to restore to the cursor */
69    private Cursor.Savepoint _savepoint;
70    /** ColumnMatcher to be used when matching column values */
71    private ColumnMatcher _columnMatcher;
72  
73    public CursorBuilder(Table table) {
74      _table = (TableImpl)table;
75    }
76  
77    /**
78     * Sets the cursor so that it will start at the beginning (unless a
79     * savepoint is given).
80     */
81    public CursorBuilder beforeFirst() {
82      _beforeFirst = true;
83      return this;
84    }
85    
86    /**
87     * Sets the cursor so that it will start at the end (unless a savepoint is
88     * given).
89     */
90    public CursorBuilder afterLast() {
91      _beforeFirst = false;
92      return this;
93    }
94  
95    /**
96     * Sets a savepoint to restore for the initial position of the cursor.
97     */
98    public CursorBuilder restoreSavepoint(Cursor.Savepoint savepoint) {
99      _savepoint = savepoint;
100     return this;
101   }
102 
103   /**
104    * Sets an index to use for the cursor.
105    */
106   public CursorBuilder setIndex(Index index) {
107     _index = (IndexImpl)index;
108     return this;
109   }
110 
111   /**
112    * Sets an index to use for the cursor by searching the table for an index
113    * with the given name.
114    * @throws IllegalArgumentException if no index can be found on the table
115    *         with the given name
116    */
117   public CursorBuilder setIndexByName(String indexName) {
118     return setIndex(_table.getIndex(indexName));
119   }
120 
121   /**
122    * Sets an index to use for the cursor by searching the table for an index
123    * with exactly the given columns.
124    * @throws IllegalArgumentException if no index can be found on the table
125    *         with the given columns
126    */
127   public CursorBuilder setIndexByColumnNames(String... columnNames) {
128     return setIndexByColumns(Arrays.asList(columnNames));
129   }
130 
131   /**
132    * Sets an index to use for the cursor by searching the table for an index
133    * with exactly the given columns.
134    * @throws IllegalArgumentException if no index can be found on the table
135    *         with the given columns
136    */
137   public CursorBuilder setIndexByColumns(Column... columns) {
138     List<String> colNames = new ArrayList<String>();
139     for(Column col : columns) {
140       colNames.add(col.getName());
141     }
142     return setIndexByColumns(colNames);
143   }
144 
145   /**
146    * Searches for an index with the given column names.
147    */
148   private CursorBuilder setIndexByColumns(List<String> searchColumns) {
149     IndexImpl index = _table.findIndexForColumns(
150         searchColumns, TableImpl.IndexFeature.ANY_MATCH);
151     if(index == null) {
152       throw new IllegalArgumentException("Index with columns " +
153                                          searchColumns +
154                                          " does not exist in table " + _table);
155     }
156     _index = index;
157     return this;
158   }
159 
160   /**
161    * Sets the starting and ending row for a range based index cursor.
162    * <p>
163    * A valid index must be specified before calling this method.
164    */
165   public CursorBuilder setSpecificRow(Object... specificRow) {
166     setStartRow(specificRow);
167     setEndRow(specificRow);
168     return this;
169   }
170   
171   /**
172    * Sets the starting and ending row for a range based index cursor to the
173    * given entry (where the given values correspond to the index's columns).
174    * <p>
175    * A valid index must be specified before calling this method.
176    */
177   public CursorBuilder setSpecificEntry(Object... specificEntry) {
178     if(specificEntry != null) {
179       setSpecificRow(_index.constructIndexRowFromEntry(specificEntry));
180     }
181     return this;
182   }
183 
184   
185   /**
186    * Sets the starting row for a range based index cursor.
187    * <p>
188    * A valid index must be specified before calling this method.
189    */
190   public CursorBuilder setStartRow(Object... startRow) {
191     _startRow = startRow;
192     return this;
193   }
194   
195   /**
196    * Sets the starting row for a range based index cursor to the given entry
197    * (where the given values correspond to the index's columns).
198    * <p>
199    * A valid index must be specified before calling this method.
200    */
201   public CursorBuilder setStartEntry(Object... startEntry) {
202     if(startEntry != null) {
203       setStartRow(_index.constructPartialIndexRowFromEntry(
204                       IndexData.MIN_VALUE, startEntry));
205     }
206     return this;
207   }
208 
209   /**
210    * Sets whether the starting row for a range based index cursor is inclusive
211    * or exclusive.
212    */
213   public CursorBuilder setStartRowInclusive(boolean inclusive) {
214     _startRowInclusive = inclusive;
215     return this;
216   }
217 
218   /**
219    * Sets the ending row for a range based index cursor.
220    * <p>
221    * A valid index must be specified before calling this method.
222    */
223   public CursorBuilder setEndRow(Object... endRow) {
224     _endRow = endRow;
225     return this;
226   }
227   
228   /**
229    * Sets the ending row for a range based index cursor to the given entry
230    * (where the given values correspond to the index's columns).
231    * <p>
232    * A valid index must be specified before calling this method.
233    */
234   public CursorBuilder setEndEntry(Object... endEntry) {
235     if(endEntry != null) {
236       setEndRow(_index.constructPartialIndexRowFromEntry(
237                     IndexData.MAX_VALUE, endEntry));
238     }
239     return this;
240   }
241 
242   /**
243    * Sets whether the ending row for a range based index cursor is inclusive
244    * or exclusive.
245    */
246   public CursorBuilder setEndRowInclusive(boolean inclusive) {
247     _endRowInclusive = inclusive;
248     return this;
249   }
250 
251   /**
252    * Sets the ColumnMatcher to use for matching row patterns.
253    */
254   public CursorBuilder setColumnMatcher(ColumnMatcher columnMatcher) {
255     _columnMatcher = columnMatcher;
256     return this;
257   }
258 
259   /**
260    * Returns a new cursor for the table, constructed to the given
261    * specifications.
262    */
263   public Cursor toCursor() throws IOException
264   {
265     CursorImpl cursor = null;
266     if(_index == null) {
267       cursor = CursorImpl.createCursor(_table);
268     } else {
269       cursor = IndexCursorImpl.createCursor(_table, _index,
270                                             _startRow, _startRowInclusive,
271                                             _endRow, _endRowInclusive);
272     }
273     cursor.setColumnMatcher(_columnMatcher);
274     if(_savepoint == null) {
275       if(!_beforeFirst) {
276         cursor.afterLast();
277       }
278     } else {
279       cursor.restoreSavepoint(_savepoint);
280     }
281     return cursor;
282   }
283   
284   /**
285    * Returns a new index cursor for the table, constructed to the given
286    * specifications.
287    */
288   public IndexCursor toIndexCursor() throws IOException
289   {
290     return (IndexCursorImpl)toCursor();
291   }
292 
293   /**
294    * Creates a normal, un-indexed cursor for the given table.
295    * @param table the table over which this cursor will traverse
296    */
297   public static Cursor createCursor(Table table) throws IOException {
298     return table.newCursor().toCursor();
299   }
300 
301   /**
302    * Creates an indexed cursor for the given table.
303    * <p>
304    * Note, index based table traversal may not include all rows, as certain
305    * types of indexes do not include all entries (namely, some indexes ignore
306    * null entries, see {@link Index#shouldIgnoreNulls}).
307    * 
308    * @param index index for the table which will define traversal order as
309    *              well as enhance certain lookups
310    */
311   public static IndexCursor createCursor(Index index)
312     throws IOException
313   {
314     return index.getTable().newCursor().setIndex(index).toIndexCursor();
315   }
316 
317   /**
318    * Creates an indexed cursor for the primary key cursor of the given table.
319    * @param table the table over which this cursor will traverse
320    */
321   public static IndexCursor createPrimaryKeyCursor(Table table)
322     throws IOException
323   {
324     return createCursor(table.getPrimaryKeyIndex());
325   }
326   
327   /**
328    * Creates an indexed cursor for the given table, narrowed to the given
329    * range.
330    * <p>
331    * Note, index based table traversal may not include all rows, as certain
332    * types of indexes do not include all entries (namely, some indexes ignore
333    * null entries, see {@link Index#shouldIgnoreNulls}).
334    * 
335    * @param index index for the table which will define traversal order as
336    *              well as enhance certain lookups
337    * @param startRow the first row of data for the cursor (inclusive), or
338    *                 {@code null} for the first entry
339    * @param endRow the last row of data for the cursor (inclusive), or
340    *               {@code null} for the last entry
341    */
342   public static IndexCursor createCursor(Index index,
343                                          Object[] startRow, Object[] endRow)
344     throws IOException
345   {
346     return index.getTable().newCursor().setIndex(index)
347       .setStartRow(startRow)
348       .setEndRow(endRow)
349       .toIndexCursor();
350   }
351   
352   /**
353    * Creates an indexed cursor for the given table, narrowed to the given
354    * range.
355    * <p>
356    * Note, index based table traversal may not include all rows, as certain
357    * types of indexes do not include all entries (namely, some indexes ignore
358    * null entries, see {@link Index#shouldIgnoreNulls}).
359    * 
360    * @param index index for the table which will define traversal order as
361    *              well as enhance certain lookups
362    * @param startRow the first row of data for the cursor, or {@code null} for
363    *                 the first entry
364    * @param startInclusive whether or not startRow is inclusive or exclusive
365    * @param endRow the last row of data for the cursor, or {@code null} for
366    *               the last entry
367    * @param endInclusive whether or not endRow is inclusive or exclusive
368    */
369   public static IndexCursor createCursor(Index index,
370                                          Object[] startRow,
371                                          boolean startInclusive,
372                                          Object[] endRow,
373                                          boolean endInclusive)
374     throws IOException
375   {
376     return index.getTable().newCursor().setIndex(index)
377       .setStartRow(startRow)
378       .setStartRowInclusive(startInclusive)
379       .setEndRow(endRow)
380       .setEndRowInclusive(endInclusive)
381       .toIndexCursor();
382   }
383 
384   /**
385    * Convenience method for finding a specific row in a table which matches a
386    * given row "pattern".  See {@link Cursor#findFirstRow(Map)} for details on
387    * the rowPattern.
388    * <p>
389    * Warning, this method <i>always</i> starts searching from the beginning of
390    * the Table (you cannot use it to find successive matches).
391    * 
392    * @param table the table to search
393    * @param rowPattern pattern to be used to find the row
394    * @return the matching row or {@code null} if a match could not be found.
395    */
396   public static Row findRow(Table table, Map<String,?> rowPattern)
397     throws IOException
398   {
399     Cursor cursor = createCursor(table);
400     if(cursor.findFirstRow(rowPattern)) {
401       return cursor.getCurrentRow();
402     }
403     return null;
404   }
405   
406   /**
407    * Convenience method for finding a specific row (as defined by the cursor)
408    * where the index entries match the given values.  See {@link
409    * IndexCursor#findRowByEntry(Object...)} for details on the entryValues.
410    * 
411    * @param index the index to search
412    * @param entryValues the column values for the index's columns.
413    * @return the matching row or {@code null} if a match could not be found.
414    */
415   public static Row findRowByEntry(Index index, Object... entryValues)
416     throws IOException
417   {
418     return createCursor(index).findRowByEntry(entryValues);
419   }
420   
421   /**
422    * Convenience method for finding a specific row by the primary key of the
423    * table.  See {@link IndexCursor#findRowByEntry(Object...)} for details on
424    * the entryValues.
425    * 
426    * @param table the table to search
427    * @param entryValues the column values for the table's primary key columns.
428    * @return the matching row or {@code null} if a match could not be found.
429    */
430   public static Row findRowByPrimaryKey(Table table, Object... entryValues)
431     throws IOException
432   {
433     return findRowByEntry(table.getPrimaryKeyIndex(), entryValues);
434   }
435   
436   /**
437    * Convenience method for finding a specific row in a table which matches a
438    * given row "pattern".  See {@link Cursor#findFirstRow(Column,Object)} for
439    * details on the pattern.
440    * <p>
441    * Note, a {@code null} result value is ambiguous in that it could imply no
442    * match or a matching row with {@code null} for the desired value.  If
443    * distinguishing this situation is important, you will need to use a Cursor
444    * directly instead of this convenience method.
445    * 
446    * @param table the table to search
447    * @param column column whose value should be returned
448    * @param columnPattern column being matched by the valuePattern
449    * @param valuePattern value from the columnPattern which will match the
450    *                     desired row
451    * @return the matching row or {@code null} if a match could not be found.
452    */
453   public static Object findValue(Table table, Column column,
454                                  Column columnPattern, Object valuePattern)
455     throws IOException
456   {
457     Cursor cursor = createCursor(table);
458     if(cursor.findFirstRow(columnPattern, valuePattern)) {
459       return cursor.getCurrentRowValue(column);
460     }
461     return null;
462   }
463   
464   /**
465    * Convenience method for finding a specific row in an indexed table which
466    * matches a given row "pattern".  See {@link Cursor#findFirstRow(Map)} for
467    * details on the rowPattern.
468    * <p>
469    * Warning, this method <i>always</i> starts searching from the beginning of
470    * the Table (you cannot use it to find successive matches).
471    * 
472    * @param index index to assist the search
473    * @param rowPattern pattern to be used to find the row
474    * @return the matching row or {@code null} if a match could not be found.
475    */
476   public static Row findRow(Index index, Map<String,?> rowPattern)
477     throws IOException
478   {
479     Cursor cursor = createCursor(index);
480     if(cursor.findFirstRow(rowPattern)) {
481       return cursor.getCurrentRow();
482     }
483     return null;
484   }
485   
486   /**
487    * Convenience method for finding a specific row in a table which matches a
488    * given row "pattern".  See {@link Cursor#findFirstRow(Column,Object)} for
489    * details on the pattern.
490    * <p>
491    * Note, a {@code null} result value is ambiguous in that it could imply no
492    * match or a matching row with {@code null} for the desired value.  If
493    * distinguishing this situation is important, you will need to use a Cursor
494    * directly instead of this convenience method.
495    * 
496    * @param index index to assist the search
497    * @param column column whose value should be returned
498    * @param columnPattern column being matched by the valuePattern
499    * @param valuePattern value from the columnPattern which will match the
500    *                     desired row
501    * @return the matching row or {@code null} if a match could not be found.
502    */
503   public static Object findValue(Index index, Column column,
504                                  Column columnPattern, Object valuePattern)
505     throws IOException
506   {
507     Cursor cursor = createCursor(index);
508     if(cursor.findFirstRow(columnPattern, valuePattern)) {
509       return cursor.getCurrentRowValue(column);
510     }
511     return null;
512   }
513 }