diff --git a/src/main/java/io/ebean/LikeType.java b/src/main/java/io/ebean/LikeType.java index 5ddbcbef6..e2927a750 100644 --- a/src/main/java/io/ebean/LikeType.java +++ b/src/main/java/io/ebean/LikeType.java @@ -6,22 +6,22 @@ package io.ebean; public enum LikeType { /** - * You need to put in your own wildcards. + * You need to put in your own wildcards - no escaping is performed. */ RAW, /** - * The % wildcard is added to the end of the search word. + * The % wildcard is added to the end of the search word and search word is escaped. */ STARTS_WITH, /** - * The % wildcard is added to the beginning of the search word. + * The % wildcard is added to the beginning of the search word and search word is escaped. */ ENDS_WITH, /** - * The % wildcard is added to the beginning and end of the search word. + * The % wildcard is added to the beginning and end of the search word and search word is escaped. */ CONTAINS, diff --git a/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java index ce49742f6..60ea6bd45 100644 --- a/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java @@ -174,6 +174,8 @@ public class DatabasePlatform { protected boolean supportsNativeIlike; protected SqlExceptionTranslator exceptionTranslator = new SqlCodeTranslator(); + + protected char[] specialLikeCharacters = { '%', '_' }; /** * Instantiates a new database platform. @@ -617,4 +619,37 @@ public class DatabasePlatform { logger.error("Error closing resultSet", e); } } + + /** + * Escapes the like string for this DB-Platform + */ + public String escapeLikeString(String value) { + StringBuilder sb = null; + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + boolean escaped = false; + for (char escapeChar: specialLikeCharacters) { + if (ch == escapeChar) { + if (sb == null) { + sb = new StringBuilder(value.substring(0, i)); + } + escapeLikeCharacter(escapeChar, sb); + escaped = true; + break; + } + } + if (!escaped && sb != null) { + sb.append(ch); + } + } + if (sb == null) { + return value; + } else { + return sb.toString(); + } + } + + protected void escapeLikeCharacter(char ch, StringBuilder sb) { + sb.append('\\').append(ch); + } } diff --git a/src/main/java/io/ebean/config/dbplatform/h2/H2Platform.java b/src/main/java/io/ebean/config/dbplatform/h2/H2Platform.java index c8317ea75..8cff6db10 100644 --- a/src/main/java/io/ebean/config/dbplatform/h2/H2Platform.java +++ b/src/main/java/io/ebean/config/dbplatform/h2/H2Platform.java @@ -40,10 +40,6 @@ public class H2Platform extends DatabasePlatform { this.dbIdentity.setSupportsSequence(true); this.dbIdentity.setSupportsIdentity(true); - // like ? escape'' not working in the latest version H2 so just using no - // escape clause for now noting that backslash is an escape char for like in H2 - this.likeClause = "like ?"; - dbTypeMap.put(DbType.UUID, new DbPlatformType("uuid", false)); } diff --git a/src/main/java/io/ebean/config/dbplatform/mysql/MySqlPlatform.java b/src/main/java/io/ebean/config/dbplatform/mysql/MySqlPlatform.java index 2bbf39409..54ad3ebe4 100644 --- a/src/main/java/io/ebean/config/dbplatform/mysql/MySqlPlatform.java +++ b/src/main/java/io/ebean/config/dbplatform/mysql/MySqlPlatform.java @@ -30,7 +30,6 @@ public class MySqlPlatform extends DatabasePlatform { super(); this.platform = Platform.MYSQL; this.useExtraTransactionOnIterateSecondaryQueries = true; - this.likeClause = "like ? escape''"; this.selectCountWithAlias = true; this.dbEncrypt = new MySqlDbEncrypt(); this.platformDdl = new MySqlDdl(this); diff --git a/src/main/java/io/ebean/config/dbplatform/postgres/PostgresPlatform.java b/src/main/java/io/ebean/config/dbplatform/postgres/PostgresPlatform.java index 750b4e14e..920cf9f61 100644 --- a/src/main/java/io/ebean/config/dbplatform/postgres/PostgresPlatform.java +++ b/src/main/java/io/ebean/config/dbplatform/postgres/PostgresPlatform.java @@ -28,7 +28,6 @@ public class PostgresPlatform extends DatabasePlatform { super(); this.platform = Platform.POSTGRES; this.supportsNativeIlike = true; - this.likeClause = "like ? escape''"; this.selectCountWithAlias = true; this.blobDbType = Types.LONGVARBINARY; this.clobDbType = Types.VARCHAR; diff --git a/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerPlatform.java b/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerPlatform.java index 609c06042..fdb7663c1 100644 --- a/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerPlatform.java +++ b/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerPlatform.java @@ -41,6 +41,8 @@ public class SqlServerPlatform extends DatabasePlatform { this.openQuote = "["; this.closeQuote = "]"; + this.specialLikeCharacters = new char[] { '%', '_', '[' }; + this.likeClause = "like ? COLLATE Latin1_General_BIN"; booleanDbType = Types.INTEGER; dbTypeMap.put(DbType.BOOLEAN, new DbPlatformType("bit default 0")); @@ -63,4 +65,9 @@ public class SqlServerPlatform extends DatabasePlatform { } + @Override + protected void escapeLikeCharacter(char ch, StringBuilder sb) { + sb.append('[').append(ch).append(']'); + } + } diff --git a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java index e99c164af..09487e833 100644 --- a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java @@ -65,4 +65,9 @@ public interface SpiExpressionRequest { * Append a DB Like clause. */ void appendLike(); + + /** + * Escapes a string to use it as exact match in Like clause. + */ + String escapeLikeString(String value); } diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index f31eaaadd..27572d037 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -96,6 +96,14 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe public String getDBLikeClause() { return ebeanServer.getDatabasePlatform().getLikeClause(); } + + /** + * Return the database platform escaped like string. + */ + @Override + public String escapeLikeString(String value) { + return ebeanServer.getDatabasePlatform().escapeLikeString(value); + } @Override public void executeSecondaryQueries(boolean forEach) { diff --git a/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java index ecf3afdc9..ff6c8a285 100644 --- a/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -123,6 +123,11 @@ public interface SpiOrmQueryRequest extends DocQueryRequest { * Return the Database platform like clause. */ String getDBLikeClause(); + + /** + * Escapes a string to use it as exact match in Like clause. + */ + String escapeLikeString(String value); /** * Mark the underlying transaction as not being query only. diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java index d0ad74eb5..e15ab261a 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java @@ -93,6 +93,11 @@ public class DefaultExpressionRequest implements SpiExpressionRequest { sql.append(" "); } + @Override + public String escapeLikeString(String value) { + return queryRequest.escapeLikeString(value); + } + /** * Increments the parameter index and returns that value. */ diff --git a/src/main/java/io/ebeaninternal/server/expression/LikeExpression.java b/src/main/java/io/ebeaninternal/server/expression/LikeExpression.java index 4f864f0dd..b1828d8e2 100644 --- a/src/main/java/io/ebeaninternal/server/expression/LikeExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/LikeExpression.java @@ -34,8 +34,7 @@ class LikeExpression extends AbstractValueExpression { String encryptKey = prop.getBeanProperty().getEncryptKey().getStringValue(); request.addBindEncryptKey(encryptKey); } - - String bindValue = getValue(strValue(), caseInsensitive, type); + String bindValue = getValue(strValue(), caseInsensitive, type, request); request.addBindValue(bindValue); } @@ -92,13 +91,15 @@ class LikeExpression extends AbstractValueExpression { return strValue().equals(that.strValue()); } - private static String getValue(String value, boolean caseInsensitive, LikeType type) { + private static String getValue(String value, boolean caseInsensitive, LikeType type, SpiExpressionRequest request) { if (caseInsensitive) { value = value.toLowerCase(); } + if (type == LikeType.RAW) { + return value; + } + value = request.escapeLikeString(value); switch (type) { - case RAW: - return value; case STARTS_WITH: return value + "%"; case ENDS_WITH: diff --git a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java index 9bc3ea9d7..55641a1a8 100644 --- a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java +++ b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java @@ -78,4 +78,9 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest { public void appendLike() { } + + @Override + public String escapeLikeString(String value) { + return value; + } } diff --git a/src/test/java/org/tests/query/other/TestLikeEscaping.java b/src/test/java/org/tests/query/other/TestLikeEscaping.java new file mode 100644 index 000000000..a9fbc5fe9 --- /dev/null +++ b/src/test/java/org/tests/query/other/TestLikeEscaping.java @@ -0,0 +1,82 @@ +package org.tests.query.other; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import org.tests.model.basic.Customer; +import org.tests.model.basic.ResetBasicData; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestLikeEscaping extends BaseTestCase { + + + @Test + public void testLikeEscaping() { + ResetBasicData.reset(); + // Mysql, PgSql, H2 special chars: "_" "%" + // MsSql special chars: "_" "%" "[" AND different Quoting! + Ebean.save(ResetBasicData.createCustomer("Paul % Percentage", "*Star", "[none]", 0, null)); + Ebean.save(ResetBasicData.createCustomer("(none)", "More * Star", "[none]", 0, null)); + + Ebean.save(ResetBasicData.createCustomer("Paul %% Doublepercentage", "|Pipeway", "[other]", 1, null)); + Ebean.save(ResetBasicData.createCustomer("_Udo Underscore", "|Pipeway", "[other]", 1, null)); + + + assertThat(Ebean.find(Customer.class) + .where().contains("name", "Paul %%").findCount() + ).isEqualTo(1); + + + assertThat(Ebean.find(Customer.class) + .where().startsWith("name", "_").findCount() + ).isEqualTo(1); + + assertThat(Ebean.find(Customer.class) + .where().startsWith("name", "_u").findCount() + ).isEqualTo(0); + + assertThat(Ebean.find(Customer.class) + .where().istartsWith("name", "_U").findCount() + ).isEqualTo(1); + + assertThat(Ebean.find(Customer.class) + .where().startsWith("shippingAddress.line1", "|p").findCount() + ).isEqualTo(0); + + assertThat(Ebean.find(Customer.class) + .where().startsWith("shippingAddress.line1", "|P").findCount() + ).isEqualTo(2); + + + assertThat(Ebean.find(Customer.class) + .where().endsWith("billingAddress.line1", "]").findCount() + ).isEqualTo(4); + + assertThat(Ebean.find(Customer.class) + .where().endsWith("billingAddress.line1", "[none]").findCount() + ).isEqualTo(2); + + assertThat(Ebean.find(Customer.class) + .where().startsWith("billingAddress.line1", "[none]").findCount() + ).isEqualTo(2); + + assertThat(Ebean.find(Customer.class) + .where().contains("billingAddress.line1", "[none]").findCount() + ).isEqualTo(2); + + + assertThat(Ebean.find(Customer.class) + .where().contains("shippingAddress.line1", "*").findCount() + ).isEqualTo(2); + + assertThat(Ebean.find(Customer.class) + .where().startsWith("shippingAddress.line1", "*").findCount() + ).isEqualTo(1); + + assertThat(Ebean.find(Customer.class) + .where().endsWith("shippingAddress.line1", "*").findCount() + ).isEqualTo(0); + } + +}