Skip to content

Commit 1c4d07d

Browse files
committed
Added support for java.time.* methods in lambdas.
1 parent d3894f8 commit 1c4d07d

File tree

8 files changed

+383
-287
lines changed

8 files changed

+383
-287
lines changed

README.MD

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ The interfaces ``SqlPredicate<T>`` and ``SqlFunction<T>`` can be used exactly li
4444
Features
4545
---------
4646

47-
Current version works with predicates, functions and supports the following operators: >, >=, <, <=, =, !=, &&, ||, !
47+
Current version works with predicates, functions and supports the following operators: >, >=, <, <=, =, !=, &&, ||, !. The DateTime API introduced in Java 8 is also supported.
4848

4949
It is also possible to achieve ``LIKE`` operations using the String ``startsWith``, ``endsWith`` and ``contains`` methods.
5050
For example, the lambda expression\
@@ -62,7 +62,7 @@ You can include the Maven dependency:
6262
<dependency>
6363
<groupId>com.github.collinalpert</groupId>
6464
<artifactId>lambda2sql</artifactId>
65-
<version>2.0</version>
65+
<version>2.1</version>
6666
</dependency>
6767
```
6868

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.github.collinalpert</groupId>
88
<artifactId>lambda2sql</artifactId>
9-
<version>2.0</version>
9+
<version>2.1</version>
1010
<packaging>jar</packaging>
1111

1212
<name>lambda2sql</name>
@@ -60,13 +60,13 @@
6060
<dependency>
6161
<groupId>com.trigersoft</groupId>
6262
<artifactId>jaque</artifactId>
63-
<version>2.4.0</version>
63+
<version>2.4.3</version>
6464
</dependency>
6565

6666
<dependency>
6767
<groupId>org.junit.jupiter</groupId>
6868
<artifactId>junit-jupiter-api</artifactId>
69-
<version>5.2.0</version>
69+
<version>5.3.2</version>
7070
<scope>test</scope>
7171
</dependency>
7272

src/main/java/com/github/collinalpert/lambda2sql/Lambda2Sql.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class Lambda2Sql {
2121
*/
2222
public static String toSql(SerializedFunctionalInterface functionalInterface, String prefix) {
2323
var lambdaExpression = LambdaExpression.parse(functionalInterface);
24-
return lambdaExpression.accept(new ToSqlVisitor(prefix)).toString();
24+
return lambdaExpression.accept(new SqlVisitor(prefix)).toString();
2525
}
2626

2727
public static String toSql(SerializedFunctionalInterface functionalInterface) {
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package com.github.collinalpert.lambda2sql;
2+
3+
import com.github.collinalpert.lambda2sql.functions.TriFunction;
4+
import com.trigersoft.jaque.expression.BinaryExpression;
5+
import com.trigersoft.jaque.expression.ConstantExpression;
6+
import com.trigersoft.jaque.expression.DelegateExpression;
7+
import com.trigersoft.jaque.expression.Expression;
8+
import com.trigersoft.jaque.expression.ExpressionType;
9+
import com.trigersoft.jaque.expression.ExpressionVisitor;
10+
import com.trigersoft.jaque.expression.InvocationExpression;
11+
import com.trigersoft.jaque.expression.LambdaExpression;
12+
import com.trigersoft.jaque.expression.MemberExpression;
13+
import com.trigersoft.jaque.expression.ParameterExpression;
14+
import com.trigersoft.jaque.expression.UnaryExpression;
15+
16+
import java.lang.reflect.Member;
17+
import java.time.LocalDate;
18+
import java.time.LocalDateTime;
19+
import java.time.LocalTime;
20+
import java.time.chrono.ChronoLocalDate;
21+
import java.time.chrono.ChronoLocalDateTime;
22+
import java.time.temporal.Temporal;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.StringJoiner;
27+
import java.util.stream.Collectors;
28+
29+
/**
30+
* Converts a lambda expression to an SQL where condition.
31+
*/
32+
public class SqlVisitor implements ExpressionVisitor<StringBuilder> {
33+
34+
/**
35+
* The supported methods that can be used on Java objects inside the lambda expressions.
36+
*/
37+
private static final Map<Member, Integer> registeredMethods = new HashMap<>() {{
38+
try {
39+
put(String.class.getDeclaredMethod("equals", Object.class), ExpressionType.Equal);
40+
put(Object.class.getDeclaredMethod("equals", Object.class), ExpressionType.Equal);
41+
put(LocalDate.class.getDeclaredMethod("isAfter", ChronoLocalDate.class), ExpressionType.GreaterThan);
42+
put(LocalTime.class.getDeclaredMethod("isAfter", LocalTime.class), ExpressionType.GreaterThan);
43+
put(LocalDateTime.class.getDeclaredMethod("isAfter", ChronoLocalDateTime.class), ExpressionType.GreaterThan);
44+
put(LocalDate.class.getDeclaredMethod("isBefore", ChronoLocalDate.class), ExpressionType.LessThan);
45+
put(LocalTime.class.getDeclaredMethod("isBefore", LocalTime.class), ExpressionType.LessThan);
46+
put(LocalDateTime.class.getDeclaredMethod("isBefore", ChronoLocalDateTime.class), ExpressionType.LessThan);
47+
} catch (NoSuchMethodException e) {
48+
e.printStackTrace();
49+
}
50+
}};
51+
52+
private final String prefix;
53+
private final LinkedListStack<List<ConstantExpression>> arguments;
54+
55+
/**
56+
* More complex methods that can be used on Java objects inside the lambda expressions.
57+
*/
58+
private final Map<Member, TriFunction<Expression, Expression, Boolean, StringBuilder>> complexMethods = new HashMap<>() {{
59+
try {
60+
put(String.class.getDeclaredMethod("startsWith", String.class), SqlVisitor.this::stringStartsWith);
61+
put(String.class.getDeclaredMethod("endsWith", String.class), SqlVisitor.this::stringEndsWith);
62+
put(String.class.getDeclaredMethod("contains", CharSequence.class), SqlVisitor.this::stringContains);
63+
put(List.class.getDeclaredMethod("contains", Object.class), SqlVisitor.this::listContains);
64+
} catch (NoSuchMethodException e) {
65+
e.printStackTrace();
66+
}
67+
}};
68+
private StringBuilder sb;
69+
private Expression body;
70+
private Expression javaMethodParameter;
71+
72+
SqlVisitor(String prefix) {
73+
this(prefix, new LinkedListStack<>());
74+
}
75+
76+
private SqlVisitor(String prefix, LinkedListStack<List<ConstantExpression>> arguments) {
77+
this.prefix = prefix;
78+
this.arguments = arguments;
79+
this.sb = new StringBuilder();
80+
}
81+
82+
/**
83+
* Converts a Java operator to an SQL operator.
84+
*
85+
* @param expressionType The {@link ExpressionType} representing the Java expression.
86+
* @return A {@link String} which will be inserted into the query.
87+
*/
88+
private static String toSqlOperator(int expressionType) {
89+
switch (expressionType) {
90+
case ExpressionType.Equal:
91+
return "=";
92+
case ExpressionType.LogicalAnd:
93+
return "AND";
94+
case ExpressionType.LogicalOr:
95+
return "OR";
96+
case ExpressionType.IsNull:
97+
return " IS NULL";
98+
case ExpressionType.IsNonNull:
99+
return " IS NOT NULL";
100+
case ExpressionType.Convert:
101+
return "";
102+
default:
103+
return ExpressionType.toString(expressionType);
104+
}
105+
}
106+
107+
/**
108+
* Converts a binary expression to the SQL equivalent.
109+
* For example:
110+
* {@code person -> person.getId() == 2}
111+
* becomes: id = 2
112+
*
113+
* @param e the {@link BinaryExpression} to convert
114+
* @return the {@link StringBuilder} containing the where condition.
115+
*/
116+
@Override
117+
public StringBuilder visit(BinaryExpression e) {
118+
//Handling for null parameters
119+
if (e.getSecond() instanceof ParameterExpression && !arguments.top().isEmpty() && arguments.top().get(((ParameterExpression) e.getSecond()).getIndex()).getValue() == null) {
120+
return Expression.unary(e.getExpressionType() == ExpressionType.Equal ? ExpressionType.IsNull : ExpressionType.IsNonNull, Boolean.TYPE, e.getFirst()).accept(this);
121+
}
122+
123+
boolean quote = e != this.body && e.getExpressionType() == ExpressionType.LogicalOr;
124+
125+
if (quote) sb.append('(');
126+
127+
e.getFirst().accept(this);
128+
129+
sb.append(' ').append(toSqlOperator(e.getExpressionType())).append(' ');
130+
131+
e.getSecond().accept(this);
132+
133+
if (quote) sb.append(')');
134+
135+
return sb;
136+
}
137+
138+
/**
139+
* Returns a constant used in a lambda expression as the SQL equivalent.
140+
*
141+
* @param e The {@link ConstantExpression} to transform.
142+
* @return A {@link StringBuilder} that has this constant appended.
143+
*/
144+
@Override
145+
public StringBuilder visit(ConstantExpression e) {
146+
if (e.getValue() == null) {
147+
return sb.append("NULL");
148+
}
149+
150+
if (e.getValue() instanceof LambdaExpression) {
151+
return ((LambdaExpression) e.getValue()).getBody().accept(this);
152+
}
153+
154+
if (e.getValue() instanceof String || e.getValue() instanceof Temporal) {
155+
return sb.append("'").append(e.getValue()).append("'");
156+
}
157+
158+
return sb.append(e.getValue().toString());
159+
}
160+
161+
/**
162+
* An expression which represents an invocation of a lambda expression.
163+
* It is the last {@link #visit} where the arguments of {@link ParameterExpression}s are available which is why
164+
* they are temporarily saved in a list to be inserted into the SQL where condition later.
165+
*
166+
* @param e The {@link InvocationExpression} to convert.
167+
* @return A {@link StringBuilder} containing the body/target of the lambda expression.
168+
*/
169+
@Override
170+
public StringBuilder visit(InvocationExpression e) {
171+
if (e.getTarget() instanceof LambdaExpression) {
172+
var list = e.getArguments()
173+
.stream()
174+
.filter(x -> x instanceof ConstantExpression)
175+
.map(ConstantExpression.class::cast)
176+
.collect(Collectors.toList());
177+
if (!list.isEmpty()) {
178+
arguments.push(list);
179+
}
180+
}
181+
182+
if (e.getTarget().getExpressionType() == ExpressionType.MethodAccess && !e.getArguments().isEmpty()) {
183+
javaMethodParameter = e.getArguments().get(0);
184+
}
185+
186+
return e.getTarget().accept(this);
187+
}
188+
189+
/**
190+
* The entry point for converting lambda expressions.
191+
*
192+
* @param e The entire lambda expression to convert.
193+
* @return A {@link StringBuilder} containing the body of the lambda expression.
194+
*/
195+
@Override
196+
public StringBuilder visit(LambdaExpression<?> e) {
197+
if (this.body == null && e.getBody() instanceof BinaryExpression) {
198+
this.body = e.getBody();
199+
}
200+
201+
return e.getBody().accept(this);
202+
}
203+
204+
@Override
205+
public StringBuilder visit(DelegateExpression e) {
206+
return e.getDelegate().accept(this);
207+
}
208+
209+
/**
210+
* An expression which represents a getter, and thus a field in a database, in the lambda expression.
211+
* For example:
212+
* {@code person -> person.getName();}
213+
* becomes: name
214+
*
215+
* @param e The {@link MemberExpression} to convert.
216+
* @return A {@link StringBuilder} with the name of the database field appended.
217+
*/
218+
@Override
219+
public StringBuilder visit(MemberExpression e) {
220+
if (registeredMethods.containsKey(e.getMember())) {
221+
return Expression.binary(registeredMethods.get(e.getMember()), e.getInstance(), javaMethodParameter).accept(this);
222+
}
223+
224+
if (complexMethods.containsKey(e.getMember())) {
225+
return sb.append(complexMethods.get(e.getMember()).apply(e.getInstance(), javaMethodParameter, false));
226+
}
227+
228+
var nameArray = e.getMember().getName().replaceAll("^(get)", "").toCharArray();
229+
nameArray[0] = Character.toLowerCase(nameArray[0]);
230+
var name = new String(nameArray);
231+
if (prefix == null) {
232+
return sb.append(name);
233+
}
234+
235+
return sb.append(prefix).append(".").append(name);
236+
}
237+
238+
/**
239+
* Represents a parameterized expression, for example if a variable is used in a query.
240+
*
241+
* @param e The parameterized expression.
242+
* @return The {@link StringBuilder} with the SQL equivalent appended.
243+
*/
244+
@Override
245+
public StringBuilder visit(ParameterExpression e) {
246+
arguments.top().get(e.getIndex()).accept(this);
247+
if (e.getIndex() == arguments.top().size() - 1) {
248+
arguments.pop();
249+
}
250+
251+
return sb;
252+
}
253+
254+
/**
255+
* Converts a unary expression to the SQL equivalent.
256+
* For example:
257+
* {@code person -> !person.isActive();}
258+
* becomes: !active
259+
*
260+
* @param e the {@link UnaryExpression} to convert
261+
* @return A {@link StringBuilder} with the unary expression appended.
262+
*/
263+
@Override
264+
public StringBuilder visit(UnaryExpression e) {
265+
if (e.getExpressionType() == ExpressionType.LogicalNot) {
266+
//for support for negated Java methods
267+
var invocationExpression = (InvocationExpression) e.getFirst();
268+
var memberExpression = (MemberExpression) invocationExpression.getTarget();
269+
if (registeredMethods.containsKey(memberExpression.getMember())) {
270+
return Expression.logicalNot(Expression.binary(registeredMethods.get(memberExpression.getMember()), memberExpression.getInstance(), invocationExpression.getArguments().get(0))).accept(this);
271+
} else if (complexMethods.containsKey(memberExpression.getMember())) {
272+
return sb.append(complexMethods.get(memberExpression.getMember()).apply(memberExpression.getInstance(), invocationExpression.getArguments().get(0), true));
273+
} else {
274+
sb.append("!");
275+
}
276+
277+
return e.getFirst().accept(this);
278+
}
279+
280+
e.getFirst().accept(this);
281+
return sb.append(toSqlOperator(e.getExpressionType()));
282+
}
283+
284+
//region Complex Java methods
285+
286+
private StringBuilder stringStartsWith(Expression e, Expression e1, boolean negated) {
287+
var valueBuilder = e1.accept(new SqlVisitor(this.prefix, this.arguments));
288+
valueBuilder.insert(valueBuilder.length() - 1, '%');
289+
return e.accept(new SqlVisitor(this.prefix, this.arguments)).append(negated ? " NOT" : "").append(" LIKE ").append(valueBuilder);
290+
}
291+
292+
private StringBuilder stringEndsWith(Expression e, Expression e1, boolean negated) {
293+
return e.accept(new SqlVisitor(this.prefix, this.arguments)).append(negated ? " NOT" : "").append(" LIKE ").append(e1.accept(new SqlVisitor(this.prefix, this.arguments)).insert(1, '%'));
294+
}
295+
296+
private StringBuilder stringContains(Expression e, Expression e1, boolean negated) {
297+
var valueBuilder = e1.accept(new SqlVisitor(this.prefix, this.arguments));
298+
valueBuilder.insert(1, '%').insert(valueBuilder.length() - 1, '%');
299+
return e.accept(new SqlVisitor(this.prefix, this.arguments)).append(negated ? " NOT" : "").append(" LIKE ").append(valueBuilder);
300+
}
301+
302+
private StringBuilder listContains(Expression e, Expression e1, boolean negated) {
303+
List l = (List) arguments.pop().get(((ParameterExpression) e).getIndex()).getValue();
304+
var joiner = new StringJoiner(", ", "(", ")");
305+
l.forEach(x -> joiner.add(x.toString()));
306+
return e1.accept(new SqlVisitor(this.prefix, this.arguments)).append(negated ? " NOT" : "").append(" IN ").append(joiner.toString());
307+
}
308+
309+
//endregion
310+
}

0 commit comments

Comments
 (0)