diff --git a/src/main/java/com/avaje/ebean/ExpressionFactory.java b/src/main/java/com/avaje/ebean/ExpressionFactory.java index 264d87312..a21c2ee20 100644 --- a/src/main/java/com/avaje/ebean/ExpressionFactory.java +++ b/src/main/java/com/avaje/ebean/ExpressionFactory.java @@ -192,6 +192,11 @@ public interface ExpressionFactory { */ ExampleExpression exampleLike(Object example, boolean caseInsensitive, LikeType likeType); + /** + * Create the query by Example expression specifying more options. + */ + Expression like(LikeType likeType, String propertyName, Object value); + /** * Like - property like value where the value contains the SQL wild card * characters % (percentage) and _ (underscore). diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiNamedParam.java b/src/main/java/com/avaje/ebeaninternal/api/SpiNamedParam.java new file mode 100644 index 000000000..2206c5301 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiNamedParam.java @@ -0,0 +1,6 @@ +package com.avaje.ebeaninternal.api; + +public interface SpiNamedParam { + + Object getValue(); +} diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 545101630..feb9cfb92 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -296,6 +296,8 @@ public interface SpiQuery extends Query { */ void setLoadDescription(String loadMode, String loadDescription); + SpiNamedParam createNamedParameter(String parameterName); + /** * Return the joins required to support predicates on the many properties. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java index f8e6e5729..e1cd4c1fc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -251,6 +251,12 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { return new DefaultExampleExpression(checkEntityBean(example), caseInsensitive, likeType); } + @Override + public Expression like(LikeType likeType, String propertyName, Object value) { + + return new LikeExpression(propertyName, value, true, LikeType.RAW); + } + /** * Like - property like value where the value contains the SQL wild card * characters % (percentage) and _ (underscore). diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java index b8b54d3e5..5d14a5d67 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java @@ -4,28 +4,43 @@ import com.avaje.ebean.LikeType; import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; import com.avaje.ebeaninternal.api.SpiExpression; import com.avaje.ebeaninternal.api.SpiExpressionRequest; +import com.avaje.ebeaninternal.api.SpiNamedParam; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import java.io.IOException; class LikeExpression extends AbstractExpression { - private final String val; + private final Object value; private final boolean caseInsensitive; private final LikeType type; - LikeExpression(String propertyName, String value, boolean caseInsensitive, LikeType type) { + LikeExpression(String propertyName, Object value, boolean caseInsensitive, LikeType type) { super(propertyName); this.caseInsensitive = caseInsensitive; this.type = type; - this.val = value; + this.value = value; + } + + /** + * Return the bind value taking into account named parameters. + */ + private Object value() { + if (value instanceof SpiNamedParam) { + return ((SpiNamedParam)value).getValue(); + } + return value; + } + + private String val() { + return value().toString(); } @Override public void writeDocQuery(DocQueryContext context) throws IOException { - context.writeLike(propName, val, type, caseInsensitive); + context.writeLike(propName, val(), type, caseInsensitive); } @Override @@ -38,7 +53,7 @@ class LikeExpression extends AbstractExpression { request.addBindEncryptKey(encryptKey); } - String bindValue = getValue(val, caseInsensitive, type); + String bindValue = getValue(val(), caseInsensitive, type); request.addBindValue(bindValue); } @@ -74,7 +89,7 @@ class LikeExpression extends AbstractExpression { @Override public int queryBindHash() { - return val.hashCode(); + return val().hashCode(); } @Override @@ -92,7 +107,7 @@ class LikeExpression extends AbstractExpression { @Override public boolean isSameByBind(SpiExpression other) { LikeExpression that = (LikeExpression) other; - return val.equals(that.val); + return val().equals(that.val()); } private static String getValue(String value, boolean caseInsensitive, LikeType type) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java index 044b446e6..bc4804eba 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java @@ -5,6 +5,7 @@ import com.avaje.ebean.plugin.ExpressionPath; import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; import com.avaje.ebeaninternal.api.SpiExpression; import com.avaje.ebeaninternal.api.SpiExpressionRequest; +import com.avaje.ebeaninternal.api.SpiNamedParam; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import java.io.IOException; @@ -13,18 +14,28 @@ public class SimpleExpression extends AbstractExpression { private final Op type; - private final Object value; + private final Object val; public SimpleExpression(String propertyName, Op type, Object value) { super(propertyName); this.type = type; - this.value = value; + this.val = value; + } + + /** + * Return the bind value taking into account named parameters. + */ + private Object value() { + if (val instanceof SpiNamedParam) { + return ((SpiNamedParam)val).getValue(); + } + return val; } @Override public Object getIdEqualTo(String idName) { if (type == Op.EQ && idName.equals(propName)) { - return value; + return value(); } return null; } @@ -37,13 +48,13 @@ public class SimpleExpression extends AbstractExpression { ExpressionPath prop = context.getExpressionPath(propName); if (prop != null && prop.isAssocId()) { String idName = prop.getAssocIdExpression(propName, ""); - Object[] ids = prop.getAssocIdValues((EntityBean) value); + Object[] ids = prop.getAssocIdValues((EntityBean) value()); if (ids == null || ids.length != 1) { throw new IllegalArgumentException("Expecting 1 Id value for " + idName + " but got " + ids); } context.writeSimple(type, idName, ids[0]); } else { - context.writeSimple(type, propName, value); + context.writeSimple(type, propName, value()); } } @@ -56,7 +67,7 @@ public class SimpleExpression extends AbstractExpression { } public Object getValue() { - return value; + return value(); } @Override @@ -65,7 +76,7 @@ public class SimpleExpression extends AbstractExpression { ElPropertyValue prop = getElProp(request); if (prop != null) { if (prop.isAssocId()) { - Object[] ids = prop.getAssocIdValues((EntityBean) value); + Object[] ids = prop.getAssocIdValues((EntityBean) value()); if (ids != null) { for (int i = 0; i < ids.length; i++) { request.addBindValue(ids[i]); @@ -83,7 +94,7 @@ public class SimpleExpression extends AbstractExpression { // prop.getBeanProperty().getScalarType(); } - request.addBindValue(value); + request.addBindValue(value()); } @Override @@ -115,7 +126,7 @@ public class SimpleExpression extends AbstractExpression { @Override public int queryBindHash() { - return value.hashCode(); + return value().hashCode(); } @Override @@ -132,6 +143,6 @@ public class SimpleExpression extends AbstractExpression { @Override public boolean isSameByBind(SpiExpression other) { SimpleExpression that = (SimpleExpression) other; - return value.equals(that.value); + return value().equals(that.value()); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapter.java b/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapter.java index b3f7a6ad7..7cd369a72 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapter.java +++ b/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapter.java @@ -1,7 +1,10 @@ package com.avaje.ebeaninternal.server.grammer; +import com.avaje.ebean.Expression; import com.avaje.ebean.ExpressionList; +import com.avaje.ebean.LikeType; import com.avaje.ebean.Query; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.grammer.antlr.EQLBaseListener; import com.avaje.ebeaninternal.server.grammer.antlr.EQLLexer; import com.avaje.ebeaninternal.server.grammer.antlr.EQLParser; @@ -14,7 +17,7 @@ class EqlAdapter extends EQLBaseListener { private static final OperatorMapping operatorMapping = new OperatorMapping(); - private final Query query; + private final SpiQuery query; private final EqlAdapterHelper helper; @@ -25,7 +28,7 @@ class EqlAdapter extends EQLBaseListener { private boolean textMode; public EqlAdapter(Query query) { - this.query = query; + this.query = (SpiQuery)query; this.helper = new EqlAdapterHelper(this); } @@ -60,8 +63,7 @@ class EqlAdapter extends EQLBaseListener { /** * Push the expression list onto the appropriate stack. */ - private void pushExprList(ExpressionList list, String type) { - System.out.println("Push " + type + ">> "); + private void pushExprList(ExpressionList list) { if (textMode) { textStack.push(list); } else { @@ -72,8 +74,7 @@ class EqlAdapter extends EQLBaseListener { /** * End a list of expressions added by 'OR'. */ - private void popJunction(String type) { - System.out.println("Pop " + type + " >> "); + private void popJunction() { if (textMode) { textStack.pop(); } else { @@ -145,7 +146,7 @@ class EqlAdapter extends EQLBaseListener { String operator = ctx.getChild(1).getText(); EqlOperator op = operatorMapping.get(operator); if (op == null) { - throw new IllegalStateException("No operator found for " + op); + throw new IllegalStateException("No operator found for " + operator); } // RHS is Path, Literal or Named input parameter @@ -158,43 +159,50 @@ class EqlAdapter extends EQLBaseListener { public void enterConditional_term(EQLParser.Conditional_termContext ctx) { int childCount = ctx.getChildCount(); if (childCount > 1) { - pushExprList(peekExprList().and(), "Conjunction"); + pushExprList(peekExprList().and()); } } @Override public void exitConditional_term(EQLParser.Conditional_termContext ctx) { if (ctx.getChildCount() > 1) { - popJunction("Conjunction"); + popJunction(); } } @Override public void enterConditional_expression(EQLParser.Conditional_expressionContext ctx) { if (ctx.getChildCount() > 1) { - pushExprList(peekExprList().or(), "Disjunction"); + pushExprList(peekExprList().or()); } } @Override public void exitConditional_expression(EQLParser.Conditional_expressionContext ctx) { if (ctx.getChildCount() > 1) { - popJunction("Disjunction"); + popJunction(); } } @Override public void enterConditional_factor(EQLParser.Conditional_factorContext ctx) { if (ctx.getChildCount() > 1) { - pushExprList(peekExprList().not(), "Not"); + pushExprList(peekExprList().not()); } } @Override public void exitConditional_factor(EQLParser.Conditional_factorContext ctx) { if (ctx.getChildCount() > 1) { - popJunction("Not"); + popJunction(); } } + public Object namedParam(String parameterName) { + return query.createNamedParameter(parameterName); + } + + public Expression like(LikeType likeType, String property, Object bindValue) { + return query.getExpressionFactory().like(likeType, property, bindValue); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapterHelper.java b/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapterHelper.java index ff2064711..d3585d973 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapterHelper.java +++ b/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlAdapterHelper.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.grammer; import com.avaje.ebean.ExpressionList; +import com.avaje.ebean.LikeType; import java.math.BigDecimal; @@ -65,16 +66,16 @@ class EqlAdapterHelper { peekExprList().le(path, bind(value)); break; case LIKE: - peekExprList().like(path, bindString(value)); + addLike(LikeType.RAW, path, bind(value)); break; case CONTAINS: - peekExprList().contains(path, bindString(value)); + addLike(LikeType.CONTAINS, path, bind(value)); break; case STARTS_WITH: - peekExprList().startsWith(path, bindString(value)); + addLike(LikeType.STARTS_WITH, path, bind(value)); break; case ENDS_WITH: - peekExprList().endsWith(path, bindString(value)); + addLike(LikeType.ENDS_WITH, path, bind(value)); break; case ILIKE: peekExprList().ilike(path, bindString(value)); @@ -94,6 +95,10 @@ class EqlAdapterHelper { } + private void addLike(LikeType likeType, String path, Object bindValue) { + peekExprList().add(owner.like(likeType, path, bindValue)); + } + private String bindString(String value) { ValueType valueType = getValueType(value); switch (valueType) { @@ -120,7 +125,7 @@ class EqlAdapterHelper { case BOOL: return Boolean.parseBoolean(value); case NUMBER: return new BigDecimal(value); case STRING: return unquote(value); - case NAMED_PARAM: return new NamedParameter(value.substring(1)); + case NAMED_PARAM: return owner.namedParam(value.substring(1)); default: throw new IllegalArgumentException("Unhandled valueType "+valueType); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index c2dec2647..7a8bd1a69 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -16,6 +16,7 @@ import com.avaje.ebeaninternal.api.ManyWhereJoins; import com.avaje.ebeaninternal.api.SpiExpression; import com.avaje.ebeaninternal.api.SpiExpressionList; import com.avaje.ebeaninternal.api.SpiExpressionValidation; +import com.avaje.ebeaninternal.api.SpiNamedParam; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.api.SpiQuerySecondary; import com.avaje.ebeaninternal.server.autotune.ProfilingListener; @@ -28,6 +29,7 @@ import com.avaje.ebeaninternal.server.query.CancelableQuery; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -135,6 +137,8 @@ public class DefaultOrmQuery implements SpiQuery { */ private Object id; + private Map namedParams; + /** * Bind parameters when using the query language. */ @@ -1138,6 +1142,15 @@ public class DefaultOrmQuery implements SpiQuery { */ @Override public DefaultOrmQuery setParameter(String name, Object value) { + + if (namedParams != null) { + ONamedParam param = namedParams.get(name); + if (param != null) { + param.setValue(value); + return this; + } + } + if (bindParams == null) { bindParams = new BindParams(); } @@ -1375,6 +1388,20 @@ public class DefaultOrmQuery implements SpiQuery { this.generatedSql = generatedSql; } + @Override + public SpiNamedParam createNamedParameter(String name) { + if (namedParams == null) { + namedParams = new HashMap(); + } + + ONamedParam param = namedParams.get(name); + if (param == null) { + param = new ONamedParam(name); + namedParams.put(name, param); + } + return param; + } + @Override public void setDefaultFetchBuffer(int fetchSize) { if (bufferFetchSizeHint == 0) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java new file mode 100644 index 000000000..04fa405e7 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java @@ -0,0 +1,22 @@ +package com.avaje.ebeaninternal.server.querydefn; + +import com.avaje.ebeaninternal.api.SpiNamedParam; + +public class ONamedParam implements SpiNamedParam { + + private final String name; + + private Object value; + + public ONamedParam(String name) { + this.name = name; + } + + public void setValue(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } +} diff --git a/src/test/java/com/avaje/ebeaninternal/server/grammer/EqlParserTest.java b/src/test/java/com/avaje/ebeaninternal/server/grammer/EqlParserTest.java index 645bafae2..92a72c97b 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/grammer/EqlParserTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/grammer/EqlParserTest.java @@ -37,7 +37,7 @@ public class EqlParserTest { assertThat(query.getGeneratedSql()).contains("where t0.name = ?"); } - @Test @Ignore + @Test public void where_namedParam() throws Exception { Query query = parse("where name eq :name"); @@ -47,6 +47,16 @@ public class EqlParserTest { assertThat(query.getGeneratedSql()).contains("where t0.name = ?"); } +// @Test +// public void where_namedParam_startsWith() throws Exception { +// +// Query query = parse("where name startsWith :name"); +// query.setParameter("name", "Rob"); +// query.findList(); +// +// assertThat(query.getGeneratedSql()).contains("where t0.name like ?"); +// } + @Test public void where_or1() throws Exception {