CAY-1971 Variants of Property.like(..) : contains(..), startsWith(..), endsWith(..)
Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/9587d047 Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/9587d047 Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/9587d047 Branch: refs/heads/CAY-1946_1 Commit: 9587d0475b3c3f44150fcf8fc50ea772e9ea40ea Parents: f11fc5c Author: aadamchik <aadamc...@apache.org> Authored: Sat Nov 22 14:46:31 2014 +0300 Committer: aadamchik <aadamc...@apache.org> Committed: Sat Nov 22 16:25:50 2014 +0300 ---------------------------------------------------------------------- .../apache/cayenne/exp/ExpressionFactory.java | 179 ++++++++++++------- .../cayenne/exp/LikeExpressionHelper.java | 73 ++++++++ .../java/org/apache/cayenne/exp/Property.java | 99 +++++++++- .../org/apache/cayenne/exp/PropertyTest.java | 21 +++ 4 files changed, 298 insertions(+), 74 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cayenne/blob/9587d047/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java index dc1d815..cbef42d 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java @@ -93,20 +93,14 @@ public class ExpressionFactory { // make sure all types are small integers, then we can use // them as indexes in lookup array - int[] allTypes = new int[] { Expression.AND, Expression.OR, - Expression.NOT, Expression.EQUAL_TO, Expression.NOT_EQUAL_TO, - Expression.LESS_THAN, Expression.GREATER_THAN, - Expression.LESS_THAN_EQUAL_TO, - Expression.GREATER_THAN_EQUAL_TO, Expression.BETWEEN, - Expression.IN, Expression.LIKE, Expression.LIKE_IGNORE_CASE, - Expression.ADD, Expression.SUBTRACT, Expression.MULTIPLY, - Expression.DIVIDE, Expression.NEGATIVE, Expression.OBJ_PATH, - Expression.DB_PATH, Expression.LIST, Expression.NOT_BETWEEN, - Expression.NOT_IN, Expression.NOT_LIKE, - Expression.NOT_LIKE_IGNORE_CASE, Expression.TRUE, - Expression.FALSE, Expression.BITWISE_NOT, - Expression.BITWISE_AND, Expression.BITWISE_OR, - Expression.BITWISE_XOR, Expression.BITWISE_LEFT_SHIFT, + int[] allTypes = new int[] { Expression.AND, Expression.OR, Expression.NOT, Expression.EQUAL_TO, + Expression.NOT_EQUAL_TO, Expression.LESS_THAN, Expression.GREATER_THAN, Expression.LESS_THAN_EQUAL_TO, + Expression.GREATER_THAN_EQUAL_TO, Expression.BETWEEN, Expression.IN, Expression.LIKE, + Expression.LIKE_IGNORE_CASE, Expression.ADD, Expression.SUBTRACT, Expression.MULTIPLY, + Expression.DIVIDE, Expression.NEGATIVE, Expression.OBJ_PATH, Expression.DB_PATH, Expression.LIST, + Expression.NOT_BETWEEN, Expression.NOT_IN, Expression.NOT_LIKE, Expression.NOT_LIKE_IGNORE_CASE, + Expression.TRUE, Expression.FALSE, Expression.BITWISE_NOT, Expression.BITWISE_AND, + Expression.BITWISE_OR, Expression.BITWISE_XOR, Expression.BITWISE_LEFT_SHIFT, Expression.BITWISE_RIGHT_SHIFT }; int max = 0; @@ -322,19 +316,15 @@ public class ExpressionFactory { int splitEnd = path.indexOf(Entity.PATH_SEPARATOR, split + 1); - String beforeSplit = split > 0 ? path.substring(0, split) + "." - : ""; - String afterSplit = splitEnd > 0 ? "." - + path.substring(splitEnd + 1) : ""; + String beforeSplit = split > 0 ? path.substring(0, split) + "." : ""; + String afterSplit = splitEnd > 0 ? "." + path.substring(splitEnd + 1) : ""; String aliasBase = "split" + autoAliasId++ + "_"; - String splitChunk = splitEnd > 0 ? path.substring(split + 1, - splitEnd) : path.substring(split + 1); + String splitChunk = splitEnd > 0 ? path.substring(split + 1, splitEnd) : path.substring(split + 1); // fix the path - replace split with dot if it's in the middle, or // strip it if // it's in the beginning - path = split == 0 ? path.substring(1) : path.replace( - SPLIT_SEPARATOR, '.'); + path = split == 0 ? path.substring(1) : path.replace(SPLIT_SEPARATOR, '.'); int i = 0; for (Object value : values) { @@ -344,8 +334,7 @@ public class ExpressionFactory { i++; ASTPath pathExp = new ASTObjPath(aliasedPath); - pathExp.setPathAliases(Collections.singletonMap(alias, - splitChunk)); + pathExp.setPathAliases(Collections.singletonMap(alias, splitChunk)); matches.add(new ASTEqual(pathExp, value)); } } else { @@ -572,8 +561,7 @@ public class ExpressionFactory { /** * A convenience shortcut for building BETWEEN expressions. */ - public static Expression betweenExp(String pathSpec, Object value1, - Object value2) { + public static Expression betweenExp(String pathSpec, Object value1, Object value2) { return new ASTBetween(new ASTObjPath(pathSpec), value1, value2); } @@ -582,16 +570,14 @@ public class ExpressionFactory { * * @since 3.0 */ - public static Expression betweenDbExp(String pathSpec, Object value1, - Object value2) { + public static Expression betweenDbExp(String pathSpec, Object value1, Object value2) { return new ASTBetween(new ASTDbPath(pathSpec), value1, value2); } /** * A convenience shortcut for building NOT_BETWEEN expressions. */ - public static Expression notBetweenExp(String pathSpec, Object value1, - Object value2) { + public static Expression notBetweenExp(String pathSpec, Object value1, Object value2) { return new ASTNotBetween(new ASTObjPath(pathSpec), value1, value2); } @@ -600,8 +586,7 @@ public class ExpressionFactory { * * @since 3.0 */ - public static Expression notBetweenDbExp(String pathSpec, Object value1, - Object value2) { + public static Expression notBetweenDbExp(String pathSpec, Object value1, Object value2) { return new ASTNotBetween(new ASTDbPath(pathSpec), value1, value2); } @@ -609,7 +594,7 @@ public class ExpressionFactory { * A convenience shortcut for building LIKE expression. */ public static Expression likeExp(String pathSpec, Object value) { - return new ASTLike(new ASTObjPath(pathSpec), value); + return likeExpInternal(pathSpec, value, (char) 0); } /** @@ -624,8 +609,11 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression likeExp(String pathSpec, Object value, - char escapeChar) { + public static Expression likeExp(String pathSpec, Object value, char escapeChar) { + return likeExpInternal(pathSpec, value, escapeChar); + } + + static ASTLike likeExpInternal(String pathSpec, Object value, char escapeChar) { return new ASTLike(new ASTObjPath(pathSpec), value, escapeChar); } @@ -650,8 +638,7 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression likeDbExp(String pathSpec, Object value, - char escapeChar) { + public static Expression likeDbExp(String pathSpec, Object value, char escapeChar) { return new ASTLike(new ASTDbPath(pathSpec), value, escapeChar); } @@ -674,8 +661,7 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression notLikeExp(String pathSpec, Object value, - char escapeChar) { + public static Expression notLikeExp(String pathSpec, Object value, char escapeChar) { return new ASTNotLike(new ASTObjPath(pathSpec), value, escapeChar); } @@ -700,8 +686,7 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression notLikeDbExp(String pathSpec, Object value, - char escapeChar) { + public static Expression notLikeDbExp(String pathSpec, Object value, char escapeChar) { return new ASTNotLike(new ASTDbPath(pathSpec), value, escapeChar); } @@ -709,7 +694,7 @@ public class ExpressionFactory { * A convenience shortcut for building LIKE_IGNORE_CASE expression. */ public static Expression likeIgnoreCaseExp(String pathSpec, Object value) { - return new ASTLikeIgnoreCase(new ASTObjPath(pathSpec), value); + return likeIgnoreCaseExpInternal(pathSpec, value, (char) 0); } /** @@ -724,10 +709,12 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression likeIgnoreCaseExp(String pathSpec, Object value, - char escapeChar) { - return new ASTLikeIgnoreCase(new ASTObjPath(pathSpec), value, - escapeChar); + public static Expression likeIgnoreCaseExp(String pathSpec, Object value, char escapeChar) { + return likeIgnoreCaseExpInternal(pathSpec, value, escapeChar); + } + + static ASTLikeIgnoreCase likeIgnoreCaseExpInternal(String pathSpec, Object value, char escapeChar) { + return new ASTLikeIgnoreCase(new ASTObjPath(pathSpec), value, escapeChar); } /** @@ -751,8 +738,7 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression likeIgnoreCaseDbExp(String pathSpec, Object value, - char escapeChar) { + public static Expression likeIgnoreCaseDbExp(String pathSpec, Object value, char escapeChar) { return new ASTLikeIgnoreCase(new ASTDbPath(pathSpec), value, escapeChar); } @@ -775,10 +761,8 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression notLikeIgnoreCaseExp(String pathSpec, - Object value, char escapeChar) { - return new ASTNotLikeIgnoreCase(new ASTObjPath(pathSpec), value, - escapeChar); + public static Expression notLikeIgnoreCaseExp(String pathSpec, Object value, char escapeChar) { + return new ASTNotLikeIgnoreCase(new ASTObjPath(pathSpec), value, escapeChar); } /** @@ -786,8 +770,7 @@ public class ExpressionFactory { * * @since 3.0 */ - public static Expression notLikeIgnoreCaseDbExp(String pathSpec, - Object value) { + public static Expression notLikeIgnoreCaseDbExp(String pathSpec, Object value) { return new ASTNotLikeIgnoreCase(new ASTDbPath(pathSpec), value); } @@ -803,10 +786,77 @@ public class ExpressionFactory { * * @since 3.0.1 */ - public static Expression notLikeIgnoreCaseDbExp(String pathSpec, - Object value, char escapeChar) { - return new ASTNotLikeIgnoreCase(new ASTDbPath(pathSpec), value, - escapeChar); + public static Expression notLikeIgnoreCaseDbExp(String pathSpec, Object value, char escapeChar) { + return new ASTNotLikeIgnoreCase(new ASTDbPath(pathSpec), value, escapeChar); + } + + /** + * @return An expression for a database "LIKE" query with the value + * converted to a pattern matching anywhere in the String. + * @since 4.0 + */ + public static Expression containsExp(String pathSpec, String value) { + ASTLike like = likeExpInternal(pathSpec, value, (char) 0); + LikeExpressionHelper.toContains(like); + return like; + } + + /** + * @return An expression for a database "LIKE" query with the value + * converted to a pattern matching the beginning of the String. + * @since 4.0 + */ + public static Expression startsWithExp(String pathSpec, String value) { + ASTLike like = likeExpInternal(pathSpec, value, (char) 0); + LikeExpressionHelper.toStartsWith(like); + return like; + } + + /** + * @return An expression for a database "LIKE" query with the value + * converted to a pattern matching the beginning of the String. + * @since 4.0 + */ + public static Expression endsWithExp(String pathSpec, String value) { + ASTLike like = likeExpInternal(pathSpec, value, (char) 0); + LikeExpressionHelper.toEndsWith(like); + return like; + } + + /** + * Same as {@link #containsExp(String, String)} only using case-insensitive + * comparison. + * + * @since 4.0 + */ + public static Expression containsIgnoreCaseExp(String pathSpec, String value) { + ASTLikeIgnoreCase like = likeIgnoreCaseExpInternal(pathSpec, value, (char) 0); + LikeExpressionHelper.toContains(like); + return like; + } + + /** + * Same as {@link #startsWithExp(String, String)} only using + * case-insensitive comparison. + * + * @since 4.0 + */ + public static Expression startsWithIgnoreCaseExp(String pathSpec, String value) { + ASTLikeIgnoreCase like = likeIgnoreCaseExpInternal(pathSpec, value, (char) 0); + LikeExpressionHelper.toStartsWith(like); + return like; + } + + /** + * Same as {@link #endsWithExp(String, String)} only using case-insensitive + * comparison. + * + * @since 4.0 + */ + public static Expression endsWithIgnoreCaseExp(String pathSpec, String value) { + ASTLikeIgnoreCase like = likeIgnoreCaseExpInternal(pathSpec, value, (char) 0); + LikeExpressionHelper.toEndsWith(like); + return like; } /** @@ -837,8 +887,7 @@ public class ExpressionFactory { * expression would match any of the expressions. * </p> */ - public static Expression joinExp(int type, - Collection<Expression> expressions) { + public static Expression joinExp(int type, Collection<Expression> expressions) { int len = expressions.size(); if (len == 0) { return null; @@ -872,8 +921,7 @@ public class ExpressionFactory { * <code>object</code>. */ public static Expression matchExp(Persistent object) { - return matchAllDbExp(object.getObjectId().getIdSnapshot(), - Expression.EQUAL_TO); + return matchAllDbExp(object.getObjectId().getIdSnapshot(), Expression.EQUAL_TO); } /** @@ -971,12 +1019,11 @@ public class ExpressionFactory { // optimizing parser buffers per CAY-1667... // adding 1 extra char to the buffer size above the String length, as // otherwise resizing still occurs at the end of the stream - int bufferSize = expressionString.length() > PARSE_BUFFER_MAX_SIZE ? PARSE_BUFFER_MAX_SIZE - : expressionString.length() + 1; + int bufferSize = expressionString.length() > PARSE_BUFFER_MAX_SIZE ? PARSE_BUFFER_MAX_SIZE : expressionString + .length() + 1; Reader reader = new StringReader(expressionString); JavaCharStream stream = new JavaCharStream(reader, 1, 1, bufferSize); - ExpressionParserTokenManager tm = new ExpressionParserTokenManager( - stream); + ExpressionParserTokenManager tm = new ExpressionParserTokenManager(stream); ExpressionParser parser = new ExpressionParser(tm); try { http://git-wip-us.apache.org/repos/asf/cayenne/blob/9587d047/cayenne-server/src/main/java/org/apache/cayenne/exp/LikeExpressionHelper.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/LikeExpressionHelper.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/LikeExpressionHelper.java new file mode 100644 index 0000000..7725dc9 --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/LikeExpressionHelper.java @@ -0,0 +1,73 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cayenne.exp; + +import org.apache.cayenne.exp.parser.PatternMatchNode; + +/** + * @since 4.0 + */ +class LikeExpressionHelper { + + // presumably only "?" can't be an escape char + private static final char[] ESCAPE_ALPHABET = new char[] { '\\', '|', '/', ' ' }; + + private static final String WILDCARD_SEQUENCE = "%"; + private static final String WILDCARD_ONE = "_"; + + static void toContains(PatternMatchNode exp) { + escape(exp); + wrap(exp, true, true); + } + + static void toStartsWith(PatternMatchNode exp) { + escape(exp); + wrap(exp, false, true); + } + + static void toEndsWith(PatternMatchNode exp) { + escape(exp); + wrap(exp, true, false); + } + + static void escape(PatternMatchNode exp) { + + } + + static void wrap(PatternMatchNode exp, boolean start, boolean end) { + + Object pattern = exp.getOperand(1); + if (pattern instanceof String) { + + StringBuilder buffer = new StringBuilder(); + if (start) { + buffer.append(WILDCARD_SEQUENCE); + } + + buffer.append(pattern); + + if (end) { + buffer.append(WILDCARD_SEQUENCE); + } + + exp.setOperand(1, buffer.toString()); + } + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/9587d047/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java index b42e890..1676315 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java @@ -165,34 +165,117 @@ public class Property<E> { } /** - * @return An expression for a Database "Like" query. + * @param pattern + * a pattern matching property value. Pattern may include "_" and + * "%" wildcard symbols to match any single character or a + * sequence of characters. To prevent "_" and "%" from being + * treated as wildcards, they need to be escaped and escape char + * passed with {@link #like(String, char)} method. + * @return An expression for a Database "LIKE" query. */ - public Expression like(E value) { - return ExpressionFactory.likeExp(getName(), value); + public Expression like(String pattern) { + return ExpressionFactory.likeExp(getName(), pattern); } /** - * @return An expression for a case insensitive "Like" query. + * @param pattern + * a properly escaped pattern matching property value. Pattern + * may include "_" and "%" wildcard symbols to match any single + * character or a sequence of characters. + * @param escapeChar + * an escape character used in the pattern to escape "%" and "_". + * + * @return An expression for a Database "LIKE" query. + */ + public Expression like(String pattern, char escapeChar) { + return ExpressionFactory.likeExp(getName(), pattern, escapeChar); + } + + /** + * @return An expression for a case insensitive "LIKE" query. */ - public Expression likeInsensitive(E value) { - return ExpressionFactory.likeIgnoreCaseExp(getName(), value); + public Expression likeInsensitive(String pattern) { + return ExpressionFactory.likeIgnoreCaseExp(getName(), pattern); } /** * @return An expression for a Database "NOT LIKE" query. */ - public Expression nlike(E value) { + public Expression nlike(String value) { return ExpressionFactory.notLikeExp(getName(), value); } /** * @return An expression for a case insensitive "NOT LIKE" query. */ - public Expression nlikeInsensitive(E value) { + public Expression nlikeInsensitive(String value) { return ExpressionFactory.notLikeIgnoreCaseExp(getName(), value); } /** + * Creates an expression for a database "LIKE" query with the value + * converted to a pattern matching anywhere in the String. + * + * @param substring + * a String to match against property value. "_" and "%" symbols + * are NOT treated as wildcards and are escaped when converted to + * a LIKE expression. + */ + public Expression contains(String substring) { + return ExpressionFactory.containsExp(getName(), substring); + } + + /** + * Creates an expression for a database "LIKE" query with the value + * converted to a pattern matching the beginning of a String. + * + * @param substring + * a String to match against property value. "_" and "%" symbols + * are NOT treated as wildcards and are escaped when converted to + * a LIKE expression. + */ + public Expression startsWith(String value) { + return ExpressionFactory.startsWithExp(getName(), value); + } + + /** + * Creates an expression for a database "LIKE" query with the value + * converted to a pattern matching the tail of a String. + * + * @param substring + * a String to match against property value. "_" and "%" symbols + * are NOT treated as wildcards and are escaped when converted to + * a LIKE expression. + */ + public Expression endsWith(String value) { + return ExpressionFactory.endsWithExp(getName(), value); + } + + /** + * Same as {@link #contains(String)}, only using case-insensitive + * comparison. + */ + public Expression icontains(String value) { + return ExpressionFactory.containsIgnoreCaseExp(getName(), value); + } + + /** + * Same as {@link #startsWith(String)}, only using case-insensitive + * comparison. + */ + public Expression istartsWith(String value) { + return ExpressionFactory.startsWithIgnoreCaseExp(getName(), value); + } + + /** + * Same as {@link #endsWith(String)}, only using case-insensitive + * comparison. + */ + public Expression iendsWith(String value) { + return ExpressionFactory.endsWithIgnoreCaseExp(getName(), value); + } + + /** * @return An expression checking for objects between a lower and upper * bound inclusive * http://git-wip-us.apache.org/repos/asf/cayenne/blob/9587d047/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java index 1144449..70fd3ce 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java @@ -192,4 +192,25 @@ public class PropertyTest { Expression e = p.like("ab%c"); assertEquals("prop like \"ab%c\"", e.toString()); } + + @Test + public void testContains() { + Property<String> p = new Property<String>("prop"); + Expression e = p.contains("abc"); + assertEquals("prop like \"%abc%\"", e.toString()); + } + + @Test + public void testStartsWith() { + Property<String> p = new Property<String>("prop"); + Expression e = p.startsWith("abc"); + assertEquals("prop like \"abc%\"", e.toString()); + } + + @Test + public void testEndsWith() { + Property<String> p = new Property<String>("prop"); + Expression e = p.endsWith("abc"); + assertEquals("prop like \"%abc\"", e.toString()); + } }