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