You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

IndexCursor.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. /*
  2. Copyright (c) 2011 James Ahlborn
  3. This library is free software; you can redistribute it and/or
  4. modify it under the terms of the GNU Lesser General Public
  5. License as published by the Free Software Foundation; either
  6. version 2.1 of the License, or (at your option) any later version.
  7. This library is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  10. Lesser General Public License for more details.
  11. You should have received a copy of the GNU Lesser General Public
  12. License along with this library; if not, write to the Free Software
  13. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  14. USA
  15. */
  16. package com.healthmarketscience.jackcess;
  17. import java.io.IOException;
  18. import java.util.Collection;
  19. import java.util.HashSet;
  20. import java.util.Iterator;
  21. import java.util.LinkedHashMap;
  22. import java.util.Map;
  23. import java.util.Set;
  24. import com.healthmarketscience.jackcess.Table.RowState;
  25. import org.apache.commons.logging.Log;
  26. import org.apache.commons.logging.LogFactory;
  27. /**
  28. * Cursor backed by an index with extended traversal options.
  29. *
  30. * @author James Ahlborn
  31. */
  32. public class IndexCursor extends Cursor
  33. {
  34. private static final Log LOG = LogFactory.getLog(IndexCursor.class);
  35. /** IndexDirHandler for forward traversal */
  36. private final IndexDirHandler _forwardDirHandler =
  37. new ForwardIndexDirHandler();
  38. /** IndexDirHandler for backward traversal */
  39. private final IndexDirHandler _reverseDirHandler =
  40. new ReverseIndexDirHandler();
  41. /** logical index which this cursor is using */
  42. private final Index _index;
  43. /** Cursor over the entries of the relevant index */
  44. private final IndexData.EntryCursor _entryCursor;
  45. /** column names for the index entry columns */
  46. private Set<String> _indexEntryPattern;
  47. private IndexCursor(Table table, Index index,
  48. IndexData.EntryCursor entryCursor)
  49. throws IOException
  50. {
  51. super(new Id(table, index), table,
  52. new IndexPosition(entryCursor.getFirstEntry()),
  53. new IndexPosition(entryCursor.getLastEntry()));
  54. _index = index;
  55. _index.initialize();
  56. _entryCursor = entryCursor;
  57. }
  58. /**
  59. * Creates an indexed cursor for the given table.
  60. * <p>
  61. * Note, index based table traversal may not include all rows, as certain
  62. * types of indexes do not include all entries (namely, some indexes ignore
  63. * null entries, see {@link Index#shouldIgnoreNulls}).
  64. *
  65. * @param table the table over which this cursor will traverse
  66. * @param index index for the table which will define traversal order as
  67. * well as enhance certain lookups
  68. */
  69. public static IndexCursor createCursor(Table table, Index index)
  70. throws IOException
  71. {
  72. return createCursor(table, index, null, null);
  73. }
  74. /**
  75. * Creates an indexed cursor for the given table, narrowed to the given
  76. * range.
  77. * <p>
  78. * Note, index based table traversal may not include all rows, as certain
  79. * types of indexes do not include all entries (namely, some indexes ignore
  80. * null entries, see {@link Index#shouldIgnoreNulls}).
  81. *
  82. * @param table the table over which this cursor will traverse
  83. * @param index index for the table which will define traversal order as
  84. * well as enhance certain lookups
  85. * @param startRow the first row of data for the cursor (inclusive), or
  86. * {@code null} for the first entry
  87. * @param endRow the last row of data for the cursor (inclusive), or
  88. * {@code null} for the last entry
  89. */
  90. public static IndexCursor createCursor(
  91. Table table, Index index, Object[] startRow, Object[] endRow)
  92. throws IOException
  93. {
  94. return createCursor(table, index, startRow, true, endRow, true);
  95. }
  96. /**
  97. * Creates an indexed cursor for the given table, narrowed to the given
  98. * range.
  99. * <p>
  100. * Note, index based table traversal may not include all rows, as certain
  101. * types of indexes do not include all entries (namely, some indexes ignore
  102. * null entries, see {@link Index#shouldIgnoreNulls}).
  103. *
  104. * @param table the table over which this cursor will traverse
  105. * @param index index for the table which will define traversal order as
  106. * well as enhance certain lookups
  107. * @param startRow the first row of data for the cursor, or {@code null} for
  108. * the first entry
  109. * @param startInclusive whether or not startRow is inclusive or exclusive
  110. * @param endRow the last row of data for the cursor, or {@code null} for
  111. * the last entry
  112. * @param endInclusive whether or not endRow is inclusive or exclusive
  113. */
  114. public static IndexCursor createCursor(Table table, Index index,
  115. Object[] startRow,
  116. boolean startInclusive,
  117. Object[] endRow,
  118. boolean endInclusive)
  119. throws IOException
  120. {
  121. if(table != index.getTable()) {
  122. throw new IllegalArgumentException(
  123. "Given index is not for given table: " + index + ", " + table);
  124. }
  125. if(!table.getFormat().INDEXES_SUPPORTED) {
  126. throw new IllegalArgumentException(
  127. "JetFormat " + table.getFormat() +
  128. " does not currently support index lookups");
  129. }
  130. if(index.getIndexData().isReadOnly()) {
  131. throw new IllegalArgumentException(
  132. "Given index " + index +
  133. " is not usable for indexed lookups because it is read-only");
  134. }
  135. IndexCursor cursor = new IndexCursor(table, index,
  136. index.cursor(startRow, startInclusive,
  137. endRow, endInclusive));
  138. // init the column matcher appropriately for the index type
  139. cursor.setColumnMatcher(null);
  140. return cursor;
  141. }
  142. public Index getIndex() {
  143. return _index;
  144. }
  145. /**
  146. * @deprecated renamed to {@link #findFirstRowByEntry(Object...)} to be more
  147. * clear
  148. */
  149. @Deprecated
  150. public boolean findRowByEntry(Object... entryValues)
  151. throws IOException
  152. {
  153. return findFirstRowByEntry(entryValues);
  154. }
  155. /**
  156. * Moves to the first row (as defined by the cursor) where the index entries
  157. * match the given values. If a match is not found (or an exception is
  158. * thrown), the cursor is restored to its previous state.
  159. * <p>
  160. * Warning, this method <i>always</i> starts searching from the beginning of
  161. * the Table (you cannot use it to find successive matches).
  162. *
  163. * @param entryValues the column values for the index's columns.
  164. * @return {@code true} if a valid row was found with the given values,
  165. * {@code false} if no row was found
  166. */
  167. public boolean findFirstRowByEntry(Object... entryValues)
  168. throws IOException
  169. {
  170. Position curPos = _curPos;
  171. Position prevPos = _prevPos;
  172. boolean found = false;
  173. try {
  174. found = findFirstRowByEntryImpl(toRowValues(entryValues), true);
  175. return found;
  176. } finally {
  177. if(!found) {
  178. try {
  179. restorePosition(curPos, prevPos);
  180. } catch(IOException e) {
  181. LOG.error("Failed restoring position", e);
  182. }
  183. }
  184. }
  185. }
  186. /**
  187. * Moves to the first row (as defined by the cursor) where the index entries
  188. * are >= the given values. If a an exception is thrown, the cursor is
  189. * restored to its previous state.
  190. *
  191. * @param entryValues the column values for the index's columns.
  192. */
  193. public void findClosestRowByEntry(Object... entryValues)
  194. throws IOException
  195. {
  196. Position curPos = _curPos;
  197. Position prevPos = _prevPos;
  198. boolean found = false;
  199. try {
  200. findFirstRowByEntryImpl(toRowValues(entryValues), false);
  201. found = true;
  202. } finally {
  203. if(!found) {
  204. try {
  205. restorePosition(curPos, prevPos);
  206. } catch(IOException e) {
  207. LOG.error("Failed restoring position", e);
  208. }
  209. }
  210. }
  211. }
  212. /**
  213. * Returns {@code true} if the current row matches the given index entries.
  214. *
  215. * @param entryValues the column values for the index's columns.
  216. */
  217. public boolean currentRowMatchesEntry(Object... entryValues)
  218. throws IOException
  219. {
  220. return currentRowMatchesEntryImpl(toRowValues(entryValues));
  221. }
  222. /**
  223. * Returns a modifiable Iterator which will iterate through all the rows of
  224. * this table which match the given index entries.
  225. * @throws IllegalStateException if an IOException is thrown by one of the
  226. * operations, the actual exception will be contained within
  227. */
  228. public Iterator<Map<String,Object>> entryIterator(Object... entryValues)
  229. {
  230. return entryIterator((Collection<String>)null, entryValues);
  231. }
  232. /**
  233. * Returns a modifiable Iterator which will iterate through all the rows of
  234. * this table which match the given index entries, returning only the given
  235. * columns.
  236. * @throws IllegalStateException if an IOException is thrown by one of the
  237. * operations, the actual exception will be contained within
  238. */
  239. public Iterator<Map<String,Object>> entryIterator(
  240. Collection<String> columnNames, Object... entryValues)
  241. {
  242. return new EntryIterator(columnNames, toRowValues(entryValues));
  243. }
  244. /**
  245. * Returns an Iterable whose iterator() method returns the result of a call
  246. * to {@link #entryIterator(Object...)}
  247. * @throws IllegalStateException if an IOException is thrown by one of the
  248. * operations, the actual exception will be contained within
  249. */
  250. public Iterable<Map<String,Object>> entryIterable(Object... entryValues)
  251. {
  252. return entryIterable((Collection<String>)null, entryValues);
  253. }
  254. /**
  255. * Returns an Iterable whose iterator() method returns the result of a call
  256. * to {@link #entryIterator(Collection,Object...)}
  257. * @throws IllegalStateException if an IOException is thrown by one of the
  258. * operations, the actual exception will be contained within
  259. */
  260. public Iterable<Map<String,Object>> entryIterable(
  261. final Collection<String> columnNames, final Object... entryValues)
  262. {
  263. return new Iterable<Map<String, Object>>() {
  264. public Iterator<Map<String, Object>> iterator() {
  265. return new EntryIterator(columnNames, toRowValues(entryValues));
  266. }
  267. };
  268. }
  269. @Override
  270. protected IndexDirHandler getDirHandler(boolean moveForward) {
  271. return (moveForward ? _forwardDirHandler : _reverseDirHandler);
  272. }
  273. @Override
  274. protected boolean isUpToDate() {
  275. return(super.isUpToDate() && _entryCursor.isUpToDate());
  276. }
  277. @Override
  278. protected void reset(boolean moveForward) {
  279. _entryCursor.reset(moveForward);
  280. super.reset(moveForward);
  281. }
  282. @Override
  283. protected void restorePositionImpl(Position curPos, Position prevPos)
  284. throws IOException
  285. {
  286. if(!(curPos instanceof IndexPosition) ||
  287. !(prevPos instanceof IndexPosition)) {
  288. throw new IllegalArgumentException(
  289. "Restored positions must be index positions");
  290. }
  291. _entryCursor.restorePosition(((IndexPosition)curPos).getEntry(),
  292. ((IndexPosition)prevPos).getEntry());
  293. super.restorePositionImpl(curPos, prevPos);
  294. }
  295. @Override
  296. protected boolean findNextRowImpl(Column columnPattern, Object valuePattern)
  297. throws IOException
  298. {
  299. if(!isBeforeFirst()) {
  300. // use the default table scan for finding rows mid-cursor
  301. return super.findNextRowImpl(columnPattern, valuePattern);
  302. }
  303. // searching for the first match
  304. Object[] rowValues = _entryCursor.getIndexData().constructIndexRow(
  305. columnPattern.getName(), valuePattern);
  306. if(rowValues == null) {
  307. // bummer, use the default table scan
  308. return super.findNextRowImpl(columnPattern, valuePattern);
  309. }
  310. // sweet, we can use our index
  311. if(!findPotentialRow(rowValues, true)) {
  312. return false;
  313. }
  314. // either we found a row with the given value, or none exist in the
  315. // table
  316. return currentRowMatches(columnPattern, valuePattern);
  317. }
  318. /**
  319. * Moves to the first row (as defined by the cursor) where the index entries
  320. * match the given values. Caller manages save/restore on failure.
  321. *
  322. * @param rowValues the column values built from the index column values
  323. * @param requireMatch whether or not an exact match is found
  324. * @return {@code true} if a valid row was found with the given values,
  325. * {@code false} if no row was found
  326. */
  327. protected boolean findFirstRowByEntryImpl(Object[] rowValues,
  328. boolean requireMatch)
  329. throws IOException
  330. {
  331. if(!findPotentialRow(rowValues, requireMatch)) {
  332. return false;
  333. } else if(!requireMatch) {
  334. // nothing more to do, we have moved to the closest row
  335. return true;
  336. }
  337. return currentRowMatchesEntryImpl(rowValues);
  338. }
  339. @Override
  340. protected boolean findNextRowImpl(Map<String,?> rowPattern)
  341. throws IOException
  342. {
  343. if(!isBeforeFirst()) {
  344. // use the default table scan for finding rows mid-cursor
  345. return super.findNextRowImpl(rowPattern);
  346. }
  347. // searching for the first match
  348. IndexData indexData = _entryCursor.getIndexData();
  349. Object[] rowValues = indexData.constructIndexRow(rowPattern);
  350. if(rowValues == null) {
  351. // bummer, use the default table scan
  352. return super.findNextRowImpl(rowPattern);
  353. }
  354. // sweet, we can use our index
  355. if(!findPotentialRow(rowValues, true)) {
  356. // at end of index, no potential matches
  357. return false;
  358. }
  359. // find actual matching row
  360. Map<String,?> indexRowPattern = null;
  361. if(rowPattern.size() == indexData.getColumns().size()) {
  362. // the rowPattern matches our index columns exactly, so we can
  363. // streamline our testing below
  364. indexRowPattern = rowPattern;
  365. } else {
  366. // the rowPattern has more columns than just the index, so we need to
  367. // do more work when testing below
  368. Map<String,Object> tmpRowPattern = new LinkedHashMap<String,Object>();
  369. indexRowPattern = tmpRowPattern;
  370. for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) {
  371. tmpRowPattern.put(idxCol.getName(), rowValues[idxCol.getColumnIndex()]);
  372. }
  373. }
  374. // there may be multiple columns which fit the pattern subset used by
  375. // the index, so we need to keep checking until our index values no
  376. // longer match
  377. do {
  378. if(!currentRowMatches(indexRowPattern)) {
  379. // there are no more rows which could possibly match
  380. break;
  381. }
  382. // note, if rowPattern == indexRowPattern, no need to do an extra
  383. // comparison with the current row
  384. if((rowPattern == indexRowPattern) || currentRowMatches(rowPattern)) {
  385. // found it!
  386. return true;
  387. }
  388. } while(moveToNextRow());
  389. // none of the potential rows matched
  390. return false;
  391. }
  392. private boolean currentRowMatchesEntryImpl(Object[] rowValues)
  393. throws IOException
  394. {
  395. if(_indexEntryPattern == null) {
  396. // init our set of index column names
  397. _indexEntryPattern = new HashSet<String>();
  398. for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
  399. _indexEntryPattern.add(col.getName());
  400. }
  401. }
  402. // check the next row to see if it actually matches
  403. Map<String,Object> row = getCurrentRow(_indexEntryPattern);
  404. for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
  405. String columnName = col.getName();
  406. Object patValue = rowValues[col.getColumnIndex()];
  407. Object rowValue = row.get(columnName);
  408. if(!_columnMatcher.matches(getTable(), columnName,
  409. patValue, rowValue)) {
  410. return false;
  411. }
  412. }
  413. return true;
  414. }
  415. private boolean findPotentialRow(Object[] rowValues, boolean requireMatch)
  416. throws IOException
  417. {
  418. _entryCursor.beforeEntry(rowValues);
  419. IndexData.Entry startEntry = _entryCursor.getNextEntry();
  420. if(requireMatch && !startEntry.getRowId().isValid()) {
  421. // at end of index, no potential matches
  422. return false;
  423. }
  424. // move to position and check it out
  425. restorePosition(new IndexPosition(startEntry));
  426. return true;
  427. }
  428. private Object[] toRowValues(Object[] entryValues)
  429. {
  430. return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues);
  431. }
  432. @Override
  433. protected Position findAnotherPosition(RowState rowState, Position curPos,
  434. boolean moveForward)
  435. throws IOException
  436. {
  437. IndexDirHandler handler = getDirHandler(moveForward);
  438. IndexPosition endPos = (IndexPosition)handler.getEndPosition();
  439. IndexData.Entry entry = handler.getAnotherEntry();
  440. return ((!entry.equals(endPos.getEntry())) ?
  441. new IndexPosition(entry) : endPos);
  442. }
  443. @Override
  444. protected ColumnMatcher getDefaultColumnMatcher() {
  445. if(getIndex().isUnique()) {
  446. // text indexes are case-insensitive, therefore we should always use a
  447. // case-insensitive matcher for unique indexes.
  448. return CaseInsensitiveColumnMatcher.INSTANCE;
  449. }
  450. return SimpleColumnMatcher.INSTANCE;
  451. }
  452. /**
  453. * Handles moving the table index cursor in a given direction. Separates
  454. * cursor logic from value storage.
  455. */
  456. private abstract class IndexDirHandler extends DirHandler {
  457. public abstract IndexData.Entry getAnotherEntry()
  458. throws IOException;
  459. }
  460. /**
  461. * Handles moving the table index cursor forward.
  462. */
  463. private final class ForwardIndexDirHandler extends IndexDirHandler {
  464. @Override
  465. public Position getBeginningPosition() {
  466. return getFirstPosition();
  467. }
  468. @Override
  469. public Position getEndPosition() {
  470. return getLastPosition();
  471. }
  472. @Override
  473. public IndexData.Entry getAnotherEntry() throws IOException {
  474. return _entryCursor.getNextEntry();
  475. }
  476. }
  477. /**
  478. * Handles moving the table index cursor backward.
  479. */
  480. private final class ReverseIndexDirHandler extends IndexDirHandler {
  481. @Override
  482. public Position getBeginningPosition() {
  483. return getLastPosition();
  484. }
  485. @Override
  486. public Position getEndPosition() {
  487. return getFirstPosition();
  488. }
  489. @Override
  490. public IndexData.Entry getAnotherEntry() throws IOException {
  491. return _entryCursor.getPreviousEntry();
  492. }
  493. }
  494. /**
  495. * Value object which maintains the current position of an IndexCursor.
  496. */
  497. private static final class IndexPosition extends Position
  498. {
  499. private final IndexData.Entry _entry;
  500. private IndexPosition(IndexData.Entry entry) {
  501. _entry = entry;
  502. }
  503. @Override
  504. public RowId getRowId() {
  505. return getEntry().getRowId();
  506. }
  507. public IndexData.Entry getEntry() {
  508. return _entry;
  509. }
  510. @Override
  511. protected boolean equalsImpl(Object o) {
  512. return getEntry().equals(((IndexPosition)o).getEntry());
  513. }
  514. @Override
  515. public String toString() {
  516. return "Entry = " + getEntry();
  517. }
  518. }
  519. /**
  520. * Row iterator (by matching entry) for this cursor, modifiable.
  521. */
  522. private final class EntryIterator extends BaseIterator
  523. {
  524. private final Object[] _rowValues;
  525. private EntryIterator(Collection<String> columnNames, Object[] rowValues)
  526. {
  527. super(columnNames);
  528. _rowValues = rowValues;
  529. try {
  530. _hasNext = findFirstRowByEntryImpl(rowValues, true);
  531. _validRow = _hasNext;
  532. } catch(IOException e) {
  533. throw new IllegalStateException(e);
  534. }
  535. }
  536. @Override
  537. protected boolean findNext() throws IOException {
  538. return (moveToNextRow() && currentRowMatchesEntryImpl(_rowValues));
  539. }
  540. }
  541. }