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