summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2011-06-15 13:21:29 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2011-06-15 13:21:29 +0000
commit69f4ea2454423c631c69444018d850428cfc09d0 (patch)
tree8694a0b996b8e3eb908fdee5b6e5a4e06a089e11 /src
parentd3ea113a92439aacd69d61445dbfcf8b14e483a3 (diff)
downloadjackcess-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.xml4
-rw-r--r--src/java/com/healthmarketscience/jackcess/Cursor.java65
-rw-r--r--src/java/com/healthmarketscience/jackcess/IndexCursor.java175
-rw-r--r--src/java/com/healthmarketscience/jackcess/Joiner.java196
-rw-r--r--src/java/com/healthmarketscience/jackcess/Table.java4
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>.