git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1328 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-3.5.0
@@ -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"> |
@@ -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; | |||
@@ -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; | |||
} | |||
} | |||
} |
@@ -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: |
@@ -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); | |||
} | |||
} |
@@ -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)); | |||
} | |||
} |
@@ -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) |
@@ -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; | |||
} | |||
} |