]> source.dussan.org Git - jackcess.git/commitdiff
Add support for using Predicates to match values in Cursors. Add PatternColumnPredic...
authorJames Ahlborn <jtahlborn@yahoo.com>
Sat, 27 Jun 2020 05:04:14 +0000 (05:04 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Sat, 27 Jun 2020 05:04:14 +0000 (05:04 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1328 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/Cursor.java
src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java
src/main/java/com/healthmarketscience/jackcess/util/PatternColumnPredicate.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
src/test/java/com/healthmarketscience/jackcess/util/PatternColumnPredicateTest.java [new file with mode: 0644]

index 8ad95653fa8aa09c91d6a4dda588b2dc776db32d..5cf9f8718e0b52795b8d7ee6a026e76abc14f594 100644 (file)
       <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">
index 89fec96b880d3802947556dcfdfc296980127d99..0573591eaa5dc3c997ec85d7b1b26a3d3adc0369 100644 (file)
@@ -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;
 
index 7b973a4eedde20d52c811c568d07c40169ad0f85..2897bbc472050f9da4148e4426460d7bc39532fb 100644 (file)
@@ -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;
     }
   }
-  
+
 }
index d9cf5b97bea23a298ef402485fd14a0660de8635..1c6d5f3f326b2de8255fec242050224cbc3eb602 100644 (file)
@@ -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:
index f527bb209d0aa4fce0f95be1908a0f97ad838ca7..4a5a5377a801be31bc2a8ef3ca78f62b4c9549af 100644 (file)
@@ -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 (file)
index 0000000..7ccbea5
--- /dev/null
@@ -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));
+  }
+}
index 6661948a4811630ac44c8be325cee254cdf0fa54..abc3364a62b052f659668afaafe99b36bf434c95 100644 (file)
@@ -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 (file)
index 0000000..a8f5523
--- /dev/null
@@ -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;
+  }
+}