aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/changes/changes.xml5
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/Cursor.java32
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java144
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java15
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java11
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/util/PatternColumnPredicate.java127
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java3
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/util/PatternColumnPredicateTest.java138
8 files changed, 390 insertions, 85 deletions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 8ad9565..5cf9f87 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -18,6 +18,11 @@
<action dev="jahlborn" type="update">
Change the default DateTimeType to LOCAL_DATE_TIME.
</action>
+ <action dev="jahlborn" type="add">
+ Add support for Predicate value patterns in cursor find methods. Add
+ PatternColumnPredicate for creating Predicate instances which can
+ match values using various pattern syntaxes.
+ </action>
</release>
<release version="3.0.1" date="2019-04-13">
<action dev="jahlborn" type="update">
diff --git a/src/main/java/com/healthmarketscience/jackcess/Cursor.java b/src/main/java/com/healthmarketscience/jackcess/Cursor.java
index 89fec96..0573591 100644
--- a/src/main/java/com/healthmarketscience/jackcess/Cursor.java
+++ b/src/main/java/com/healthmarketscience/jackcess/Cursor.java
@@ -253,7 +253,10 @@ public interface Cursor extends Iterable<Row>
* @param columnPattern column from the table for this cursor which is being
* matched by the valuePattern
* @param valuePattern value which is equal to the corresponding value in
- * the matched row
+ * the matched row. If this object is an instance of
+ * {@link java.util.function.Predicate}, it will be
+ * applied to the potential row value instead
+ * (overriding any configured ColumnMatcher)
* @return {@code true} if a valid row was found with the given value,
* {@code false} if no row was found
*/
@@ -269,7 +272,10 @@ public interface Cursor extends Iterable<Row>
* @param columnPattern column from the table for this cursor which is being
* matched by the valuePattern
* @param valuePattern value which is equal to the corresponding value in
- * the matched row
+ * the matched row. If this object is an instance of
+ * {@link java.util.function.Predicate}, it will be
+ * applied to the potential row value instead
+ * (overriding any configured ColumnMatcher)
* @return {@code true} if a valid row was found with the given value,
* {@code false} if no row was found
*/
@@ -286,7 +292,10 @@ public interface Cursor extends Iterable<Row>
* the Table (you cannot use it to find successive matches).
*
* @param rowPattern column names and values which must be equal to the
- * corresponding values in the matched row
+ * corresponding values in the matched row. If a value is
+ * an instance of {@link java.util.function.Predicate}, it
+ * will be applied to the potential row value instead
+ * (overriding any configured ColumnMatcher)
* @return {@code true} if a valid row was found with the given values,
* {@code false} if no row was found
*/
@@ -299,7 +308,10 @@ public interface Cursor extends Iterable<Row>
* is restored to its previous state.
*
* @param rowPattern column names and values which must be equal to the
- * corresponding values in the matched row
+ * corresponding values in the matched row. If a value is
+ * an instance of {@link java.util.function.Predicate}, it
+ * will be applied to the potential row value instead
+ * (overriding any configured ColumnMatcher)
* @return {@code true} if a valid row was found with the given values,
* {@code false} if no row was found
*/
@@ -309,8 +321,11 @@ public interface Cursor extends Iterable<Row>
* Returns {@code true} if the current row matches the given pattern.
* @param columnPattern column from the table for this cursor which is being
* matched by the valuePattern
- * @param valuePattern value which is tested for equality with the
- * corresponding value in the current row
+ * @param valuePattern value which is equal to the corresponding value in
+ * the matched row. If this object is an instance of
+ * {@link java.util.function.Predicate}, it will be
+ * applied to the potential row value instead
+ * (overriding any configured ColumnMatcher)
*/
public boolean currentRowMatches(Column columnPattern, Object valuePattern)
throws IOException;
@@ -318,7 +333,10 @@ public interface Cursor extends Iterable<Row>
/**
* Returns {@code true} if the current row matches the given pattern.
* @param rowPattern column names and values which must be equal to the
- * corresponding values in the current row
+ * corresponding values in the matched row. If a value is
+ * an instance of {@link java.util.function.Predicate}, it
+ * will be applied to the potential row value instead
+ * (overriding any configured ColumnMatcher)
*/
public boolean currentRowMatches(Map<String,?> rowPattern) throws IOException;
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java
index 7b973a4..2897bbc 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java
@@ -22,6 +22,7 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.function.Predicate;
import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.Cursor;
@@ -54,14 +55,14 @@ import org.apache.commons.logging.LogFactory;
* @author James Ahlborn
*/
public abstract class CursorImpl implements Cursor
-{
- private static final Log LOG = LogFactory.getLog(CursorImpl.class);
+{
+ private static final Log LOG = LogFactory.getLog(CursorImpl.class);
/** boolean value indicating forward movement */
public static final boolean MOVE_FORWARD = true;
/** boolean value indicating reverse movement */
public static final boolean MOVE_REVERSE = false;
-
+
/** identifier for this cursor */
private final IdImpl _id;
/** owning table */
@@ -101,7 +102,7 @@ public abstract class CursorImpl implements Cursor
public RowState getRowState() {
return _rowState;
}
-
+
@Override
public IdImpl getId() {
return _id;
@@ -128,7 +129,7 @@ public abstract class CursorImpl implements Cursor
@Override
public void setErrorHandler(ErrorHandler newErrorHandler) {
_rowState.setErrorHandler(newErrorHandler);
- }
+ }
@Override
public ColumnMatcher getColumnMatcher() {
@@ -173,14 +174,14 @@ public abstract class CursorImpl implements Cursor
restorePosition(savepoint.getCurrentPosition(),
savepoint.getPreviousPosition());
}
-
+
/**
* Returns the first row id (exclusive) as defined by this cursor.
*/
protected PositionImpl getFirstPosition() {
return _firstPos;
}
-
+
/**
* Returns the last row id (exclusive) as defined by this cursor.
*/
@@ -191,13 +192,13 @@ public abstract class CursorImpl implements Cursor
@Override
public void reset() {
beforeFirst();
- }
+ }
@Override
public void beforeFirst() {
reset(MOVE_FORWARD);
}
-
+
@Override
public void afterLast() {
reset(MOVE_REVERSE);
@@ -207,7 +208,7 @@ public abstract class CursorImpl implements Cursor
public boolean isBeforeFirst() throws IOException {
return isAtBeginning(MOVE_FORWARD);
}
-
+
@Override
public boolean isAfterLast() throws IOException {
return isAtBeginning(MOVE_REVERSE);
@@ -219,7 +220,7 @@ public abstract class CursorImpl implements Cursor
}
return false;
}
-
+
@Override
public boolean isCurrentRowDeleted() throws IOException
{
@@ -228,7 +229,7 @@ public abstract class CursorImpl implements Cursor
TableImpl.positionAtRowData(_rowState, _curPos.getRowId());
return _rowState.isDeleted();
}
-
+
/**
* Resets this cursor for traversing the given direction.
*/
@@ -236,8 +237,8 @@ public abstract class CursorImpl implements Cursor
_curPos = getDirHandler(moveForward).getBeginningPosition();
_prevPos = _curPos;
_rowState.reset();
- }
-
+ }
+
@Override
public Iterator<Row> iterator() {
return new RowIterator(null, true, MOVE_FORWARD);
@@ -259,8 +260,8 @@ public abstract class CursorImpl implements Cursor
Map.Entry<Column,Object> matchPattern = (Map.Entry<Column,Object>)
iterBuilder.getMatchPattern();
return new ColumnMatchIterator(
- iterBuilder.getColumnNames(), (ColumnImpl)matchPattern.getKey(),
- matchPattern.getValue(), iterBuilder.isReset(),
+ iterBuilder.getColumnNames(), (ColumnImpl)matchPattern.getKey(),
+ matchPattern.getValue(), iterBuilder.isReset(),
iterBuilder.isForward(), iterBuilder.getColumnMatcher());
}
case ROW_MATCH: {
@@ -268,14 +269,14 @@ public abstract class CursorImpl implements Cursor
Map<String,?> matchPattern = (Map<String,?>)
iterBuilder.getMatchPattern();
return new RowMatchIterator(
- iterBuilder.getColumnNames(), matchPattern,iterBuilder.isReset(),
+ iterBuilder.getColumnNames(), matchPattern,iterBuilder.isReset(),
iterBuilder.isForward(), iterBuilder.getColumnMatcher());
}
default:
throw new RuntimeException("unknown match type " + iterBuilder.getType());
}
}
-
+
@Override
public void deleteCurrentRow() throws IOException {
_table.deleteRow(_rowState, _curPos.getRowId());
@@ -287,8 +288,8 @@ public abstract class CursorImpl implements Cursor
}
@Override
- public <M extends Map<String,Object>> M updateCurrentRowFromMap(M row)
- throws IOException
+ public <M extends Map<String,Object>> M updateCurrentRowFromMap(M row)
+ throws IOException
{
return _table.updateRowFromMap(_rowState, _curPos.getRowId(), row);
}
@@ -299,7 +300,7 @@ public abstract class CursorImpl implements Cursor
}
@Override
- public Row getNextRow(Collection<String> columnNames)
+ public Row getNextRow(Collection<String> columnNames)
throws IOException
{
return getAnotherRow(columnNames, MOVE_FORWARD);
@@ -311,7 +312,7 @@ public abstract class CursorImpl implements Cursor
}
@Override
- public Row getPreviousRow(Collection<String> columnNames)
+ public Row getPreviousRow(Collection<String> columnNames)
throws IOException
{
return getAnotherRow(columnNames, MOVE_REVERSE);
@@ -327,14 +328,14 @@ public abstract class CursorImpl implements Cursor
* {@code null} if there is not another row in the given direction.
*/
private Row getAnotherRow(Collection<String> columnNames,
- boolean moveForward)
+ boolean moveForward)
throws IOException
{
if(moveToAnotherRow(moveForward)) {
return getCurrentRow(columnNames);
}
return null;
- }
+ }
@Override
public boolean moveToNextRow() throws IOException
@@ -373,12 +374,12 @@ public abstract class CursorImpl implements Cursor
{
restorePosition(curPos, _curPos);
}
-
+
/**
* Restores a current and previous position for the cursor if the given
* positions are different from the current positions.
*/
- protected final void restorePosition(PositionImpl curPos,
+ protected final void restorePosition(PositionImpl curPos,
PositionImpl prevPos)
throws IOException
{
@@ -398,7 +399,7 @@ public abstract class CursorImpl implements Cursor
_curPos = curPos;
_rowState.reset();
}
-
+
/**
* Rechecks the current position if the underlying data structures have been
* modified.
@@ -466,13 +467,13 @@ public abstract class CursorImpl implements Cursor
throws IOException
{
return findFirstRow((ColumnImpl)columnPattern, valuePattern);
- }
-
+ }
+
public boolean findFirstRow(ColumnImpl columnPattern, Object valuePattern)
throws IOException
{
return findAnotherRow(columnPattern, valuePattern, true, MOVE_FORWARD,
- _columnMatcher,
+ _columnMatcher,
prepareSearchInfo(columnPattern, valuePattern));
}
@@ -481,16 +482,16 @@ public abstract class CursorImpl implements Cursor
throws IOException
{
return findNextRow((ColumnImpl)columnPattern, valuePattern);
- }
-
+ }
+
public boolean findNextRow(ColumnImpl columnPattern, Object valuePattern)
throws IOException
{
return findAnotherRow(columnPattern, valuePattern, false, MOVE_FORWARD,
- _columnMatcher,
+ _columnMatcher,
prepareSearchInfo(columnPattern, valuePattern));
}
-
+
protected boolean findAnotherRow(ColumnImpl columnPattern, Object valuePattern,
boolean reset, boolean moveForward,
ColumnMatcher columnMatcher, Object searchInfo)
@@ -516,7 +517,7 @@ public abstract class CursorImpl implements Cursor
}
}
}
-
+
@Override
public boolean findFirstRow(Map<String,?> rowPattern) throws IOException
{
@@ -533,7 +534,7 @@ public abstract class CursorImpl implements Cursor
}
protected boolean findAnotherRow(Map<String,?> rowPattern, boolean reset,
- boolean moveForward,
+ boolean moveForward,
ColumnMatcher columnMatcher, Object searchInfo)
throws IOException
{
@@ -544,7 +545,7 @@ public abstract class CursorImpl implements Cursor
if(reset) {
reset(moveForward);
}
- found = findAnotherRowImpl(rowPattern, moveForward, columnMatcher,
+ found = findAnotherRowImpl(rowPattern, moveForward, columnMatcher,
searchInfo);
return found;
} finally {
@@ -570,17 +571,17 @@ public abstract class CursorImpl implements Cursor
{
return currentRowMatchesImpl(columnPattern, valuePattern, _columnMatcher);
}
-
- protected boolean currentRowMatchesImpl(ColumnImpl columnPattern,
+
+ protected boolean currentRowMatchesImpl(ColumnImpl columnPattern,
Object valuePattern,
ColumnMatcher columnMatcher)
throws IOException
{
- return columnMatcher.matches(getTable(), columnPattern.getName(),
- valuePattern,
- getCurrentRowValue(columnPattern));
+ return currentRowMatchesPattern(
+ columnPattern.getName(), valuePattern, columnMatcher,
+ getCurrentRowValue(columnPattern));
}
-
+
@Override
public boolean currentRowMatches(Map<String,?> rowPattern)
throws IOException
@@ -600,15 +601,28 @@ public abstract class CursorImpl implements Cursor
for(Map.Entry<String,Object> e : row.entrySet()) {
String columnName = e.getKey();
- if(!columnMatcher.matches(getTable(), columnName,
- rowPattern.get(columnName), e.getValue())) {
+ if(!currentRowMatchesPattern(columnName, rowPattern.get(columnName),
+ columnMatcher, e.getValue())) {
return false;
}
}
return true;
}
-
+
+ @SuppressWarnings("unchecked")
+ protected final boolean currentRowMatchesPattern(
+ String columnPattern, Object valuePattern,
+ ColumnMatcher columnMatcher, Object rowValue) {
+ // if the value pattern is a Predicate use that to test the value
+ if(valuePattern instanceof Predicate<?>) {
+ return ((Predicate<Object>)valuePattern).test(rowValue);
+ }
+ // otherwise, use the configured ColumnMatcher
+ return columnMatcher.matches(getTable(), columnPattern, valuePattern,
+ rowValue);
+ }
+
/**
* Moves to the next row (as defined by the cursor) where the given column
* has the given value. Caller manages save/restore on failure.
@@ -649,8 +663,8 @@ public abstract class CursorImpl implements Cursor
* @return {@code true} if a valid row was found with the given values,
* {@code false} if no row was found
*/
- protected boolean findAnotherRowImpl(Map<String,?> rowPattern,
- boolean moveForward,
+ protected boolean findAnotherRowImpl(Map<String,?> rowPattern,
+ boolean moveForward,
ColumnMatcher columnMatcher,
Object searchInfo)
throws IOException
@@ -664,7 +678,7 @@ public abstract class CursorImpl implements Cursor
}
}
return false;
- }
+ }
/**
* Called before a search commences to allow for search specific data to be
@@ -688,8 +702,8 @@ public abstract class CursorImpl implements Cursor
* Called by findAnotherRowImpl to determine if the search should continue
* after finding a row which does not match the current pattern.
*/
- protected boolean keepSearching(ColumnMatcher columnMatcher,
- Object searchInfo)
+ protected boolean keepSearching(ColumnMatcher columnMatcher,
+ Object searchInfo)
throws IOException
{
return true;
@@ -779,7 +793,7 @@ public abstract class CursorImpl implements Cursor
return(_curPos.getRowId().isValid() && !isCurrentRowDeleted() &&
!isBeforeFirst() && !isAfterLast());
}
-
+
@Override
public String toString() {
return getClass().getSimpleName() + " CurPosition " + _curPos +
@@ -790,9 +804,9 @@ public abstract class CursorImpl implements Cursor
* Returns the appropriate position information for the given row (which is
* the current row and is valid).
*/
- protected abstract PositionImpl getRowPosition(RowIdImpl rowId)
+ protected abstract PositionImpl getRowPosition(RowIdImpl rowId)
throws IOException;
-
+
/**
* Finds the next non-deleted row after the given row (as defined by this
* cursor) and returns the id of the row, where "next" may be backwards if
@@ -821,7 +835,7 @@ public abstract class CursorImpl implements Cursor
protected final ColumnMatcher _colMatcher;
protected Boolean _hasNext;
protected boolean _validRow;
-
+
protected BaseIterator(Collection<String> columnNames,
boolean reset, boolean moveForward,
ColumnMatcher columnMatcher)
@@ -850,9 +864,9 @@ public abstract class CursorImpl implements Cursor
throw new RuntimeIOException(e);
}
}
- return _hasNext;
+ return _hasNext;
}
-
+
@Override
public Row next() {
if(!hasNext()) {
@@ -884,7 +898,7 @@ public abstract class CursorImpl implements Cursor
protected abstract boolean findNext() throws IOException;
}
-
+
/**
* Row iterator for this cursor, modifiable.
*/
@@ -911,7 +925,7 @@ public abstract class CursorImpl implements Cursor
private final ColumnImpl _columnPattern;
private final Object _valuePattern;
private final Object _searchInfo;
-
+
private ColumnMatchIterator(Collection<String> columnNames,
ColumnImpl columnPattern, Object valuePattern,
boolean reset, boolean moveForward,
@@ -938,7 +952,7 @@ public abstract class CursorImpl implements Cursor
{
private final Map<String,?> _rowPattern;
private final Object _searchInfo;
-
+
private RowMatchIterator(Collection<String> columnNames,
Map<String,?> rowPattern,
boolean reset, boolean moveForward,
@@ -967,7 +981,7 @@ public abstract class CursorImpl implements Cursor
public abstract PositionImpl getEndPosition();
}
-
+
/**
* Identifier for a cursor. Will be equal to any other cursor of the same
* type for the same table. Primarily used to check the validity of a
@@ -1014,7 +1028,7 @@ public abstract class CursorImpl implements Cursor
public final int hashCode() {
return getRowId().hashCode();
}
-
+
@Override
public final boolean equals(Object o) {
return((this == o) ||
@@ -1045,7 +1059,7 @@ public abstract class CursorImpl implements Cursor
private final PositionImpl _curPos;
private final PositionImpl _prevPos;
- private SavepointImpl(IdImpl cursorId, PositionImpl curPos,
+ private SavepointImpl(IdImpl cursorId, PositionImpl curPos,
PositionImpl prevPos) {
_cursorId = cursorId;
_curPos = curPos;
@@ -1068,9 +1082,9 @@ public abstract class CursorImpl implements Cursor
@Override
public String toString() {
- return getClass().getSimpleName() + " " + _cursorId + " CurPosition " +
+ return getClass().getSimpleName() + " " + _cursorId + " CurPosition " +
_curPos + ", PrevPosition " + _prevPos;
}
}
-
+
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
index d9cf5b9..1c6d5f3 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
@@ -391,7 +391,8 @@ public class Expressionator
SpecOp.NOT_BETWEEN});
private static final Set<Character> REGEX_SPEC_CHARS = new HashSet<Character>(
- Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&'));
+ Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&',
+ '[',']','*','?'));
// this is a regular expression which will never match any string
private static final Pattern UNMATCHABLE_REGEX = Pattern.compile("(?!)");
@@ -1243,7 +1244,11 @@ public class Expressionator
.append("\"");
}
- private static Pattern likePatternToRegex(String pattern) {
+ /**
+ * Converts an ms access like pattern to a java regex, always matching case
+ * insensitively.
+ */
+ public static Pattern likePatternToRegex(String pattern) {
StringBuilder sb = new StringBuilder(pattern.length());
@@ -1289,7 +1294,7 @@ public class Expressionator
sb.append('[').append(charClass).append(']');
i += (endPos - startPos) + 1;
- } else if(REGEX_SPEC_CHARS.contains(c)) {
+ } else if(isRegexSpecialChar(c)) {
// this char is special in regexes, so escape it
sb.append('\\').append(c);
} else {
@@ -1306,6 +1311,10 @@ public class Expressionator
}
}
+ public static boolean isRegexSpecialChar(char c) {
+ return REGEX_SPEC_CHARS.contains(c);
+ }
+
private static Value toLiteralValue(Value.Type valType, Object value) {
switch(valType) {
case STRING:
diff --git a/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java
index f527bb2..4a5a537 100644
--- a/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java
+++ b/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java
@@ -28,18 +28,15 @@ import com.healthmarketscience.jackcess.impl.ColumnImpl;
* Concrete implementation of ColumnMatcher which tests textual columns
* case-insensitively ({@link DataType#TEXT} and {@link DataType#MEMO}), and
* all other columns using simple equality.
- *
+ *
* @author James Ahlborn
* @usage _general_class_
*/
public class CaseInsensitiveColumnMatcher implements ColumnMatcher {
- public static final CaseInsensitiveColumnMatcher INSTANCE =
+ public static final CaseInsensitiveColumnMatcher INSTANCE =
new CaseInsensitiveColumnMatcher();
-
- public CaseInsensitiveColumnMatcher() {
- }
@Override
public boolean matches(Table table, String columnName, Object value1,
@@ -47,7 +44,7 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher {
{
if(!table.getColumn(columnName).getType().isTextual()) {
// use simple equality
- return SimpleColumnMatcher.INSTANCE.matches(table, columnName,
+ return SimpleColumnMatcher.INSTANCE.matches(table, columnName,
value1, value2);
}
@@ -60,7 +57,7 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher {
((cs1 != null) && (cs2 != null) &&
cs1.toString().equalsIgnoreCase(cs2.toString())));
} catch(IOException e) {
- throw new RuntimeIOException("Could not read column " + columnName
+ throw new RuntimeIOException("Could not read column " + columnName
+ " value", e);
}
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/util/PatternColumnPredicate.java b/src/main/java/com/healthmarketscience/jackcess/util/PatternColumnPredicate.java
new file mode 100644
index 0000000..7ccbea5
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/util/PatternColumnPredicate.java
@@ -0,0 +1,127 @@
+/*
+Copyright (c) 2020 James Ahlborn
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package com.healthmarketscience.jackcess.util;
+
+import java.io.IOException;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+import com.healthmarketscience.jackcess.RuntimeIOException;
+import com.healthmarketscience.jackcess.impl.ColumnImpl;
+import com.healthmarketscience.jackcess.impl.expr.Expressionator;
+
+/**
+ * Predicate which tests a column value against a {@link Pattern}. The static
+ * factory methods can be used to construct the Pattern from various forms of
+ * wildcard pattern syntaxes.
+ *
+ * This class can be used as a value pattern in the various Cursor search
+ * methods, e.g. {@link com.healthmarketscience.jackcess.Cursor#findFirstRow(com.healthmarketscience.jackcess.Column,Object)}.
+ *
+ * @author James Ahlborn
+ */
+public class PatternColumnPredicate implements Predicate<Object>
+{
+ private static final int LIKE_REGEX_FLAGS = Pattern.DOTALL;
+ private static final int CI_LIKE_REGEX_FLAGS =
+ LIKE_REGEX_FLAGS | Pattern.CASE_INSENSITIVE |
+ Pattern.UNICODE_CASE;
+
+ private final Pattern _pattern;
+
+ public PatternColumnPredicate(Pattern pattern) {
+ _pattern = pattern;
+ }
+
+ @Override
+ public boolean test(Object value) {
+ try {
+ // convert column value to string
+ CharSequence cs = ColumnImpl.toCharSequence(value);
+
+ return _pattern.matcher(cs).matches();
+ } catch(IOException e) {
+ throw new RuntimeIOException("Could not coerece column value to string", e);
+ }
+ }
+
+ private static Pattern sqlLikeToRegex(
+ String value, boolean caseInsensitive)
+ {
+ StringBuilder sb = new StringBuilder(value.length());
+
+ for(int i = 0; i < value.length(); ++i) {
+ char c = value.charAt(i);
+
+ if(c == '%') {
+ sb.append(".*");
+ } else if(c == '_') {
+ sb.append('.');
+ } else if(c == '\\') {
+ if(i + 1 < value.length()) {
+ appendLiteralChar(sb, value.charAt(++i));
+ }
+ } else {
+ appendLiteralChar(sb, c);
+ }
+ }
+
+ int flags = (caseInsensitive ? CI_LIKE_REGEX_FLAGS : LIKE_REGEX_FLAGS);
+ return Pattern.compile(sb.toString(), flags);
+ }
+
+ private static void appendLiteralChar(StringBuilder sb, char c) {
+ if(Expressionator.isRegexSpecialChar(c)) {
+ sb.append('\\');
+ }
+ sb.append(c);
+ }
+
+ /**
+ * @return a PatternColumnPredicate which tests values against the given ms
+ * access wildcard pattern (always case insensitive)
+ */
+ public static PatternColumnPredicate forAccessLike(String pattern) {
+ return new PatternColumnPredicate(Expressionator.likePatternToRegex(pattern));
+ }
+
+ /**
+ * @return a PatternColumnPredicate which tests values against the given sql
+ * like pattern (supports escape char '\')
+ */
+ public static PatternColumnPredicate forSqlLike(String pattern) {
+ return forSqlLike(pattern, false);
+ }
+
+ /**
+ * @return a PatternColumnPredicate which tests values against the given sql
+ * like pattern (supports escape char '\'), optionally case
+ * insensitive
+ */
+ public static PatternColumnPredicate forSqlLike(
+ String pattern, boolean caseInsensitive) {
+ return new PatternColumnPredicate(sqlLikeToRegex(pattern, caseInsensitive));
+ }
+
+ /**
+ * @return a PatternColumnPredicate which tests values against the given
+ * java regex pattern
+ */
+ public static PatternColumnPredicate forJavaRegex(String pattern) {
+ return new PatternColumnPredicate(Pattern.compile(pattern));
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
index 6661948..abc3364 100644
--- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
+++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
@@ -38,9 +38,6 @@ public class SimpleColumnMatcher implements ColumnMatcher {
public static final SimpleColumnMatcher INSTANCE = new SimpleColumnMatcher();
- public SimpleColumnMatcher() {
- }
-
@Override
public boolean matches(Table table, String columnName, Object value1,
Object value2)
diff --git a/src/test/java/com/healthmarketscience/jackcess/util/PatternColumnPredicateTest.java b/src/test/java/com/healthmarketscience/jackcess/util/PatternColumnPredicateTest.java
new file mode 100644
index 0000000..a8f5523
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/util/PatternColumnPredicateTest.java
@@ -0,0 +1,138 @@
+/*
+Copyright (c) 2020 James Ahlborn
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package com.healthmarketscience.jackcess.util;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.ColumnBuilder;
+import com.healthmarketscience.jackcess.CursorBuilder;
+import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.Database;
+import static com.healthmarketscience.jackcess.Database.*;
+import com.healthmarketscience.jackcess.IndexCursor;
+import com.healthmarketscience.jackcess.Row;
+import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableBuilder;
+import static com.healthmarketscience.jackcess.TestUtil.*;
+import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
+import junit.framework.TestCase;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class PatternColumnPredicateTest extends TestCase
+{
+
+ public PatternColumnPredicateTest(String name) {
+ super(name);
+ }
+
+ public void testRegexPredicate() throws Exception {
+ for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+ Database db = createTestDb(fileFormat);
+
+ Table t = db.getTable("Test");
+
+ assertEquals(
+ Arrays.asList("Foo", "some row", "aNoThEr row", "nonsense"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forJavaRegex(".*o.*")));
+
+ assertEquals(
+ Arrays.asList("Bar", "0102", "FOO", "BAR", "67", "bunch_13_data", "42 is the ANSWER", "[try] matching t.h+i}s"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forJavaRegex(".*o.*").negate()));
+
+ assertEquals(
+ Arrays.asList("Foo", "some row", "FOO", "aNoThEr row", "nonsense"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forAccessLike("*o*")));
+
+ assertEquals(
+ Arrays.asList("0102", "67", "bunch_13_data", "42 is the ANSWER"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forAccessLike("*##*")));
+
+ assertEquals(
+ Arrays.asList("42 is the ANSWER"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forAccessLike("## *")));
+
+ assertEquals(
+ Arrays.asList("Foo"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forSqlLike("F_o")));
+
+ assertEquals(
+ Arrays.asList("Foo", "FOO"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forSqlLike("F_o", true)));
+
+ assertEquals(
+ Arrays.asList("[try] matching t.h+i}s"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forSqlLike("[try] % t.h+i}s")));
+
+ assertEquals(
+ Arrays.asList("bunch_13_data"),
+ findRowsByPattern(
+ t, PatternColumnPredicate.forSqlLike("bunch\\_%\\_data")));
+
+ db.close();
+ }
+ }
+
+ private static List<String> findRowsByPattern(
+ Table t, Predicate<Object> pred) {
+ return t.getDefaultCursor().newIterable()
+ .setMatchPattern("data", pred)
+ .stream()
+ .map(r -> r.getString("data"))
+ .collect(Collectors.toList());
+ }
+
+ private static Database createTestDb(FileFormat fileFormat) throws Exception {
+ Database db = create(fileFormat);
+
+ Table table = new TableBuilder("Test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true))
+ .addColumn(new ColumnBuilder("data", DataType.TEXT))
+ .setPrimaryKey("id")
+ .toTable(db);
+
+ table.addRow(Column.AUTO_NUMBER, "Foo");
+ table.addRow(Column.AUTO_NUMBER, "some row");
+ table.addRow(Column.AUTO_NUMBER, "Bar");
+ table.addRow(Column.AUTO_NUMBER, "0102");
+ table.addRow(Column.AUTO_NUMBER, "FOO");
+ table.addRow(Column.AUTO_NUMBER, "BAR");
+ table.addRow(Column.AUTO_NUMBER, "67");
+ table.addRow(Column.AUTO_NUMBER, "aNoThEr row");
+ table.addRow(Column.AUTO_NUMBER, "bunch_13_data");
+ table.addRow(Column.AUTO_NUMBER, "42 is the ANSWER");
+ table.addRow(Column.AUTO_NUMBER, "[try] matching t.h+i}s");
+ table.addRow(Column.AUTO_NUMBER, "nonsense");
+
+ return db;
+ }
+}