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 }