From 9e0b8076837b25886b48fef3c6b8e7aa9e5bfba8 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sat, 27 Jun 2020 05:04:14 +0000 Subject: [PATCH] Add support for using Predicates to match values in Cursors. Add PatternColumnPredicate for searching with various pattern syntaxes git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1328 f203690c-595d-4dc9-a70b-905162fa7fd2 --- src/changes/changes.xml | 5 + .../healthmarketscience/jackcess/Cursor.java | 32 +++- .../jackcess/impl/CursorImpl.java | 144 ++++++++++-------- .../jackcess/impl/expr/Expressionator.java | 15 +- .../util/CaseInsensitiveColumnMatcher.java | 11 +- .../jackcess/util/PatternColumnPredicate.java | 127 +++++++++++++++ .../jackcess/util/SimpleColumnMatcher.java | 3 - .../util/PatternColumnPredicateTest.java | 138 +++++++++++++++++ 8 files changed, 390 insertions(+), 85 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/util/PatternColumnPredicate.java create mode 100644 src/test/java/com/healthmarketscience/jackcess/util/PatternColumnPredicateTest.java 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 @@ Change the default DateTimeType to LOCAL_DATE_TIME. + + Add support for Predicate value patterns in cursor find methods. Add + PatternColumnPredicate for creating Predicate instances which can + match values using various pattern syntaxes. + 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 * @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 * @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 * 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 * 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 * 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 /** * 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 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 iterator() { return new RowIterator(null, true, MOVE_FORWARD); @@ -259,8 +260,8 @@ public abstract class CursorImpl implements Cursor Map.Entry matchPattern = (Map.Entry) 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 matchPattern = (Map) 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 updateCurrentRowFromMap(M row) - throws IOException + public > 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 columnNames) + public Row getNextRow(Collection columnNames) throws IOException { return getAnotherRow(columnNames, MOVE_FORWARD); @@ -311,7 +312,7 @@ public abstract class CursorImpl implements Cursor } @Override - public Row getPreviousRow(Collection columnNames) + public Row getPreviousRow(Collection 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 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 rowPattern) throws IOException { @@ -533,7 +534,7 @@ public abstract class CursorImpl implements Cursor } protected boolean findAnotherRow(Map 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 rowPattern) throws IOException @@ -600,15 +601,28 @@ public abstract class CursorImpl implements Cursor for(Map.Entry 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)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 rowPattern, - boolean moveForward, + protected boolean findAnotherRowImpl(Map 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 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 columnNames, ColumnImpl columnPattern, Object valuePattern, boolean reset, boolean moveForward, @@ -938,7 +952,7 @@ public abstract class CursorImpl implements Cursor { private final Map _rowPattern; private final Object _searchInfo; - + private RowMatchIterator(Collection columnNames, Map 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 REGEX_SPEC_CHARS = new HashSet( - 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 +{ + 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 findRowsByPattern( + Table t, Predicate 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; + } +} -- 2.39.5