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 }