diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2011-06-15 13:21:29 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2011-06-15 13:21:29 +0000 |
commit | 69f4ea2454423c631c69444018d850428cfc09d0 (patch) | |
tree | 8694a0b996b8e3eb908fdee5b6e5a4e06a089e11 /src | |
parent | d3ea113a92439aacd69d61445dbfcf8b14e483a3 (diff) | |
download | jackcess-69f4ea2454423c631c69444018d850428cfc09d0.tar.gz jackcess-69f4ea2454423c631c69444018d850428cfc09d0.zip |
Add the Joiner utility and some new methods to IndexCursor to make it easier to join tables using pre-defined (index backed) relationships.
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@565 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src')
-rw-r--r-- | src/changes/changes.xml | 4 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Cursor.java | 65 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/IndexCursor.java | 175 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Joiner.java | 196 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Table.java | 4 |
5 files changed, 384 insertions, 60 deletions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 39758a1..a030d07 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -8,6 +8,10 @@ <action dev="jahlborn" type="update"> Try multiple classloaders when loading resources as streams. </action> + <action dev="jahlborn" type="add"> + Add the Joiner utility and some new methods to IndexCursor to make it + easier to join tables using pre-defined (index backed) relationships. + </action> </release> <release version="1.2.4" date="2011-05-14"> <action dev="jahlborn" type="update"> diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index 81a2555..045fcea 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -447,7 +447,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> /** * Returns an Iterable whose iterator() method calls <code>afterLast</code> - * on this cursor and returns an unmodifiable Iterator which will iterate + * on this cursor and returns a modifiable Iterator which will iterate * through all the rows of this table in reverse order. Use of the Iterator * follows the same restrictions as a call to <code>getPreviousRow</code>. * @throws IllegalStateException if an IOException is thrown by one of the @@ -459,7 +459,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> /** * Returns an Iterable whose iterator() method calls <code>afterLast</code> - * on this table and returns an unmodifiable Iterator which will iterate + * on this table and returns a modifiable Iterator which will iterate * through all the rows of this table in reverse order, returning only the * given columns. Use of the Iterator follows the same restrictions as a * call to <code>getPreviousRow</code>. @@ -477,7 +477,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> } /** - * Calls <code>beforeFirst</code> on this cursor and returns an unmodifiable + * Calls <code>beforeFirst</code> on this cursor and returns a modifiable * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to * <code>getNextRow</code>. @@ -506,7 +506,7 @@ public abstract class Cursor implements Iterable<Map<String, Object>> } /** - * Calls <code>beforeFirst</code> on this table and returns an unmodifiable + * Calls <code>beforeFirst</code> on this table and returns a modifiable * Iterator which will iterate through all the rows of this table, returning * only the given columns. Use of the Iterator follows the same * restrictions as a call to <code>getNextRow</code>. @@ -962,26 +962,27 @@ public abstract class Cursor implements Iterable<Map<String, Object>> */ protected abstract DirHandler getDirHandler(boolean moveForward); + /** - * Row iterator for this table, modifiable. + * Base implementation of iterator for this cursor, modifiable. */ - private final class RowIterator implements Iterator<Map<String, Object>> + protected abstract class BaseIterator + implements Iterator<Map<String, Object>> { - private final Collection<String> _columnNames; - private final boolean _moveForward; - private Boolean _hasNext; + protected final Collection<String> _columnNames; + protected Boolean _hasNext; + protected boolean _validRow; - private RowIterator(Collection<String> columnNames, boolean moveForward) + protected BaseIterator(Collection<String> columnNames) { _columnNames = columnNames; - _moveForward = moveForward; - reset(_moveForward); } public boolean hasNext() { if(_hasNext == null) { try { - _hasNext = moveToAnotherRow(_moveForward); + _hasNext = findNext(); + _validRow = _hasNext; } catch(IOException e) { throw new IllegalStateException(e); } @@ -1003,12 +1004,40 @@ public abstract class Cursor implements Iterable<Map<String, Object>> } public void remove() { - try { - deleteCurrentRow(); - } catch(IOException e) { - throw new IllegalStateException(e); + if(_validRow) { + try { + deleteCurrentRow(); + _validRow = false; + } catch(IOException e) { + throw new IllegalStateException(e); + } + } else { + throw new IllegalStateException("Not at valid row"); } - } + } + + protected abstract boolean findNext() throws IOException; + } + + + /** + * Row iterator for this cursor, modifiable. + */ + private final class RowIterator extends BaseIterator + { + private final boolean _moveForward; + + private RowIterator(Collection<String> columnNames, boolean moveForward) + { + super(columnNames); + _moveForward = moveForward; + reset(_moveForward); + } + + @Override + protected boolean findNext() throws IOException { + return moveToAnotherRow(_moveForward); + } } /** diff --git a/src/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/java/com/healthmarketscience/jackcess/IndexCursor.java index 2529c40..6d02fa2 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexCursor.java +++ b/src/java/com/healthmarketscience/jackcess/IndexCursor.java @@ -20,7 +20,9 @@ USA package com.healthmarketscience.jackcess; import java.io.IOException; +import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -156,9 +158,8 @@ public class IndexCursor extends Cursor /** * Moves to the first row (as defined by the cursor) where the index entries - * match the given values (only valid for index-backed cursors). If a match - * is not found (or an exception is thrown), the cursor is restored to its - * previous state. + * match the given values. If a match is not found (or an exception is + * thrown), the cursor is restored to its previous state. * * @param entryValues the column values for the index's columns. * @return {@code true} if a valid row was found with the given values, @@ -171,7 +172,7 @@ public class IndexCursor extends Cursor Position prevPos = _prevPos; boolean found = false; try { - found = findRowByEntryImpl(entryValues, true); + found = findRowByEntryImpl(toRowValues(entryValues), true); return found; } finally { if(!found) { @@ -186,8 +187,8 @@ public class IndexCursor extends Cursor /** * Moves to the first row (as defined by the cursor) where the index entries - * are >= the given values (only valid for index-backed cursors). If a an - * exception is thrown, the cursor is restored to its previous state. + * are >= the given values. If a an exception is thrown, the cursor is + * restored to its previous state. * * @param entryValues the column values for the index's columns. */ @@ -198,7 +199,7 @@ public class IndexCursor extends Cursor Position prevPos = _prevPos; boolean found = false; try { - findRowByEntryImpl(entryValues, false); + findRowByEntryImpl(toRowValues(entryValues), false); found = true; } finally { if(!found) { @@ -210,7 +211,69 @@ public class IndexCursor extends Cursor } } } - + + /** + * Returns {@code true} if the current row matches the given index entries. + * + * @param entryValues the column values for the index's columns. + */ + public boolean currentRowMatchesEntry(Object... entryValues) + throws IOException + { + return currentRowMatchesEntryImpl(toRowValues(entryValues)); + } + + /** + * Returns a modifiable Iterator which will iterate through all the rows of + * this table which match the given index entries. + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterator<Map<String,Object>> entryIterator(Object... entryValues) + { + return entryIterator((Collection<String>)null, entryValues); + } + + /** + * Returns a modifiable Iterator which will iterate through all the rows of + * this table which match the given index entries, returning only the given + * columns. + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterator<Map<String,Object>> entryIterator( + Collection<String> columnNames, Object... entryValues) + { + return new EntryIterator(columnNames, toRowValues(entryValues)); + } + + /** + * Returns an Iterable whose iterator() method returns the result of a call + * to {@link #entryIterator(Object...)} + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterable<Map<String,Object>> entryIterable(Object... entryValues) + { + return entryIterable((Collection<String>)null, entryValues); + } + + /** + * Returns an Iterable whose iterator() method returns the result of a call + * to {@link #entryIterator(Collection,Object...)} + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterable<Map<String,Object>> entryIterable( + final Collection<String> columnNames, final Object... entryValues) + { + return new Iterable<Map<String, Object>>() { + public Iterator<Map<String, Object>> iterator() { + return new EntryIterator(columnNames, toRowValues(entryValues)); + } + }; + } + @Override protected IndexDirHandler getDirHandler(boolean moveForward) { return (moveForward ? _forwardDirHandler : _reverseDirHandler); @@ -267,19 +330,15 @@ public class IndexCursor extends Cursor * Moves to the first row (as defined by the cursor) where the index entries * match the given values. Caller manages save/restore on failure. * - * @param entryValues the column values for the index's columns. + * @param rowValues the column values built from the index column values * @param requireMatch whether or not an exact match is found * @return {@code true} if a valid row was found with the given values, * {@code false} if no row was found */ - protected boolean findRowByEntryImpl(Object[] entryValues, - boolean requireMatch) + protected boolean findRowByEntryImpl(Object[] rowValues, + boolean requireMatch) throws IOException { - Object[] rowValues = _entryCursor.getIndexData() - .constructIndexRowFromEntry(entryValues); - - // sweet, we can use our index if(!findPotentialRow(rowValues, requireMatch)) { return false; } else if(!requireMatch) { @@ -287,28 +346,7 @@ public class IndexCursor extends Cursor return true; } - if(_indexEntryPattern == null) { - // init our set of index column names - _indexEntryPattern = new HashSet<String>(); - for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { - _indexEntryPattern.add(col.getName()); - } - } - - // check the next row to see if it actually matches - Map<String,Object> row = getCurrentRow(_indexEntryPattern); - - for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { - String columnName = col.getName(); - Object patValue = rowValues[col.getColumnIndex()]; - Object rowValue = row.get(columnName); - if(!_columnMatcher.matches(getTable(), columnName, - patValue, rowValue)) { - return false; - } - } - - return true; + return currentRowMatchesEntryImpl(rowValues); } @Override @@ -338,8 +376,7 @@ public class IndexCursor extends Cursor } else { // the rowPattern has more columns than just the index, so we need to // do more work when testing below - indexRowPattern = - new LinkedHashMap<String,Object>(); + indexRowPattern = new LinkedHashMap<String,Object>(); for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) { indexRowPattern.put(idxCol.getName(), rowValues[idxCol.getColumnIndex()]); @@ -368,7 +405,34 @@ public class IndexCursor extends Cursor // none of the potential rows matched return false; } - + + private boolean currentRowMatchesEntryImpl(Object[] rowValues) + throws IOException + { + if(_indexEntryPattern == null) { + // init our set of index column names + _indexEntryPattern = new HashSet<String>(); + for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { + _indexEntryPattern.add(col.getName()); + } + } + + // check the next row to see if it actually matches + Map<String,Object> row = getCurrentRow(_indexEntryPattern); + + for(IndexData.ColumnDescriptor col : getIndex().getColumns()) { + String columnName = col.getName(); + Object patValue = rowValues[col.getColumnIndex()]; + Object rowValue = row.get(columnName); + if(!_columnMatcher.matches(getTable(), columnName, + patValue, rowValue)) { + return false; + } + } + + return true; + } + private boolean findPotentialRow(Object[] rowValues, boolean requireMatch) throws IOException { @@ -383,6 +447,11 @@ public class IndexCursor extends Cursor return true; } + private Object[] toRowValues(Object[] entryValues) + { + return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues); + } + @Override protected Position findAnotherPosition(RowState rowState, Position curPos, boolean moveForward) @@ -480,5 +549,31 @@ public class IndexCursor extends Cursor return "Entry = " + getEntry(); } } + + /** + * Row iterator (by matching entry) for this cursor, modifiable. + */ + private final class EntryIterator extends BaseIterator + { + private final Object[] _rowValues; + + private EntryIterator(Collection<String> columnNames, Object[] rowValues) + { + super(columnNames); + _rowValues = rowValues; + try { + _hasNext = findRowByEntryImpl(rowValues, true); + _validRow = _hasNext; + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + protected boolean findNext() throws IOException { + return (moveToNextRow() && currentRowMatchesEntryImpl(_rowValues)); + } + } + } diff --git a/src/java/com/healthmarketscience/jackcess/Joiner.java b/src/java/com/healthmarketscience/jackcess/Joiner.java new file mode 100644 index 0000000..afe445d --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/Joiner.java @@ -0,0 +1,196 @@ +/* +Copyright (c) 2011 James Ahlborn + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +*/ + +package com.healthmarketscience.jackcess; + +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Utility for finding rows based on pre-defined, foreign-key table + * relationships. + * + * @author James Ahlborn + */ +public class Joiner +{ + private final Index _fromIndex; + private final List<IndexData.ColumnDescriptor> _fromCols; + private final IndexCursor _toCursor; + private final Object[] _entryValues; + + private Joiner(Index fromIndex, IndexCursor toCursor) + { + _fromIndex = fromIndex; + _fromCols = _fromIndex.getColumns(); + _entryValues = new Object[_fromCols.size()]; + _toCursor = toCursor; + } + + /** + * Creates a new Joiner based on the given index which backs a foreign-key + * relationship. The table of the given index will be the "from" table and + * the table on the other end of the relationsip is the "to" table. + * + * @param fromIndex the index backing one side of a foreign-key relationship + */ + public static Joiner create(Index fromIndex) + throws IOException + { + Index toIndex = fromIndex.getReferencedIndex(); + IndexCursor toCursor = IndexCursor.createCursor( + toIndex.getTable(), toIndex); + // text lookups are always case-insensitive + toCursor.setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE); + return new Joiner(fromIndex, toCursor); + } + + public Table getFromTable() + { + return getFromIndex().getTable(); + } + + public Index getFromIndex() + { + return _fromIndex; + } + + public Table getToTable() + { + return getToCursor().getTable(); + } + + public Index getToIndex() + { + return getToCursor().getIndex(); + } + + public IndexCursor getToCursor() + { + return _toCursor; + } + + /** + * Returns the first row in the "to" table based on the given columns in the + * "from" table if any, {@code null} if there is no matching row. + * + * @param fromRow row from the "from" table (which must include the relevant + * columns for this join relationship) + */ + public Map<String,Object> findFirstRow(Map<String,Object> fromRow) + throws IOException + { + return findFirstRow(fromRow, null); + } + + /** + * Returns selected columns from the first row in the "to" table based on + * the given columns in the "from" table if any, {@code null} if there is no + * matching row. + * + * @param fromRow row from the "from" table (which must include the relevant + * columns for this join relationship) + * @param columnNames desired columns in the from table row + */ + public Map<String,Object> findFirstRow(Map<String,Object> fromRow, + Collection<String> columnNames) + throws IOException + { + toEntryValues(fromRow); + return ((_toCursor.findRowByEntry(_entryValues) ? + _toCursor.getCurrentRow(columnNames) : null)); + } + + /** + * Returns an Iterator over all the rows in the "to" table based on the + * given columns in the "from" table. + * + * @param fromRow row from the "from" table (which must include the relevant + * columns for this join relationship) + */ + public Iterator<Map<String,Object>> findRows(Map<String,Object> fromRow) + { + return findRows(fromRow, null); + } + + /** + * Returns an Iterator with the selected columns over all the rows in the + * "to" table based on the given columns in the "from" table. + * + * @param fromRow row from the "from" table (which must include the relevant + * columns for this join relationship) + * @param columnNames desired columns in the from table row + */ + public Iterator<Map<String,Object>> findRows(Map<String,Object> fromRow, + Collection<String> columnNames) + { + toEntryValues(fromRow); + return _toCursor.entryIterator(columnNames, _entryValues); + } + + /** + * Returns an Iterable whose iterator() method returns the result of a call + * to {@link #findRows(Map)} + * + * @param fromRow row from the "from" table (which must include the relevant + * columns for this join relationship) + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterable<Map<String,Object>> findRowsIterable( + Map<String,Object> fromRow) + { + return findRowsIterable(fromRow, null); + } + + /** + * Returns an Iterable whose iterator() method returns the result of a call + * to {@link #findRows(Map,Collection)} + * + * @param fromRow row from the "from" table (which must include the relevant + * columns for this join relationship) + * @param columnNames desired columns in the from table row + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterable<Map<String,Object>> findRowsIterable( + final Map<String,Object> fromRow, final Collection<String> columnNames) + { + return new Iterable<Map<String, Object>>() { + public Iterator<Map<String, Object>> iterator() { + return findRows(fromRow, columnNames); + } + }; + } + + /** + * Fills in the _entryValues with the relevant info from the given "from" + * table row. + */ + private void toEntryValues(Map<String,Object> fromRow) + { + for(int i = 0; i < _entryValues.length; ++i) { + _entryValues[i] = fromRow.get(_fromCols.get(i).getName()); + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 8808728..1d1c7a3 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -795,7 +795,7 @@ public class Table /** - * Calls <code>reset</code> on this table and returns an unmodifiable + * Calls <code>reset</code> on this table and returns a modifiable * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to * <code>getNextRow</code>. @@ -808,7 +808,7 @@ public class Table } /** - * Calls <code>reset</code> on this table and returns an unmodifiable + * Calls <code>reset</code> on this table and returns a modifiable * Iterator which will iterate through all the rows of this table, returning * only the given columns. Use of the Iterator follows the same * restrictions as a call to <code>getNextRow</code>. |