]> WPIA git - gigi.git/blob - util-testing/club/wpia/gigi/localisation/SQLTestingVisitor.java
add: improve error message on SQL syntax error
[gigi.git] / util-testing / club / wpia / gigi / localisation / SQLTestingVisitor.java
1 package club.wpia.gigi.localisation;
2
3 import java.sql.Date;
4 import java.sql.ParameterMetaData;
5 import java.sql.SQLException;
6 import java.sql.Timestamp;
7 import java.sql.Types;
8 import java.util.Arrays;
9 import java.util.Deque;
10 import java.util.LinkedList;
11 import java.util.List;
12 import java.util.concurrent.LinkedBlockingDeque;
13
14 import org.eclipse.jdt.internal.compiler.ASTVisitor;
15 import org.eclipse.jdt.internal.compiler.CompilationResult;
16 import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
17 import org.eclipse.jdt.internal.compiler.ast.BinaryExpression;
18 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
19 import org.eclipse.jdt.internal.compiler.ast.ConditionalExpression;
20 import org.eclipse.jdt.internal.compiler.ast.Expression;
21 import org.eclipse.jdt.internal.compiler.ast.ExtendedStringLiteral;
22 import org.eclipse.jdt.internal.compiler.ast.IntLiteral;
23 import org.eclipse.jdt.internal.compiler.ast.MessageSend;
24 import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
25 import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
26 import org.eclipse.jdt.internal.compiler.ast.TryStatement;
27 import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
28 import org.eclipse.jdt.internal.compiler.impl.Constant;
29 import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
30 import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
31 import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
32 import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
33 import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
34 import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
35
36 import club.wpia.gigi.database.DBEnum;
37 import club.wpia.gigi.database.DatabaseConnection;
38 import club.wpia.gigi.database.GigiPreparedStatement;
39
40 public class SQLTestingVisitor extends ASTVisitor {
41
42     private CompilationUnitDeclaration pu;
43
44     private TranslationCollector tc;
45
46     private enum Type {
47         TIMESTAMP(Types.TIMESTAMP), ENUM(Types.VARCHAR), STRING(Types.VARCHAR), BOOLEAN(Types.BOOLEAN), DATE(Types.DATE), INTEGER(Types.INTEGER), OTHER(0);
48
49         private final int sqltype;
50
51         private Type(int sqltype) {
52             this.sqltype = sqltype;
53
54         }
55
56         public void set(GigiPreparedStatement ps, int index) {
57             if (this == TIMESTAMP) {
58                 ps.setTimestamp(index, new Timestamp(System.currentTimeMillis()));
59             } else if (this == STRING) {
60                 ps.setString(index, "y");
61             } else if (this == DATE) {
62                 ps.setDate(index, new Date(System.currentTimeMillis()));
63             } else if (this == Type.BOOLEAN) {
64                 ps.setBoolean(index, false);
65             } else if (this == OTHER || this == INTEGER) {
66                 ps.setInt(index, 1000);
67             } else {
68                 throw new Error();
69             }
70         }
71
72         public boolean isOfSQLType(int i) {
73             if (i == sqltype) {
74                 return true;
75             }
76             if (i == Types.BIT && this == BOOLEAN) {
77                 return true;
78             }
79             return false;
80         }
81     }
82
83     private class TypeInstantiation {
84
85         Type type;
86
87         String enumValue;
88
89         public TypeInstantiation(Type type) {
90             this.type = type;
91         }
92
93         public TypeInstantiation(Type type, String enumValue) {
94             this.enumValue = enumValue;
95             this.type = type;
96         }
97
98         public void set(GigiPreparedStatement ps, int index) {
99             if (type == Type.ENUM) {
100                 ps.setString(index, enumValue);
101             } else {
102                 type.set(ps, index);
103             }
104         }
105
106         @Override
107         public String toString() {
108             return type.toString() + (enumValue != null ? enumValue : "");
109         }
110
111         @Override
112         public int hashCode() {
113             final int prime = 31;
114             int result = 1;
115             result = prime * result + getOuterType().hashCode();
116             result = prime * result + ((enumValue == null) ? 0 : enumValue.hashCode());
117             result = prime * result + ((type == null) ? 0 : type.hashCode());
118             return result;
119         }
120
121         @Override
122         public boolean equals(Object obj) {
123             if (this == obj) {
124                 return true;
125             }
126             if (obj == null) {
127                 return false;
128             }
129             if (getClass() != obj.getClass()) {
130                 return false;
131             }
132             TypeInstantiation other = (TypeInstantiation) obj;
133             if ( !getOuterType().equals(other.getOuterType())) {
134                 return false;
135             }
136             if (enumValue == null) {
137                 if (other.enumValue != null) {
138                     return false;
139                 }
140             } else if ( !enumValue.equals(other.enumValue)) {
141                 return false;
142             }
143             if (type != other.type) {
144                 return false;
145             }
146             return true;
147         }
148
149         private SQLTestingVisitor getOuterType() {
150             return SQLTestingVisitor.this;
151         }
152
153     }
154
155     public class SQLOccurrence {
156
157         private List<String> query;
158
159         private TryStatement target;
160
161         private CompilationResult source;
162
163         public TypeInstantiation[] types = new TypeInstantiation[10];
164
165         private int sourceStart;
166
167         public SQLOccurrence(TryStatement target) {
168             this.target = target;
169         }
170
171         public void setQuery(List<String> query, CompilationResult compilationResult, int sourceStart) {
172             this.query = query;
173             this.source = compilationResult;
174             this.sourceStart = sourceStart;
175         }
176
177         public TryStatement getTarget() {
178             return target;
179         }
180
181         public List<String> getQuery() {
182             return query;
183         }
184
185         public int getSourceStart() {
186             return sourceStart;
187         }
188
189         public boolean isQuery() {
190             return query != null;
191         }
192
193         public String getPosition() {
194             int pos = source.lineSeparatorPositions.length + 1;
195             for (int i = 0; i < source.lineSeparatorPositions.length; i++) {
196                 if (source.lineSeparatorPositions[i] > sourceStart) {
197                     pos = i + 1;
198                     break;
199                 }
200             }
201             return new String(source.getFileName()) + ":" + pos;
202         }
203
204         private void check(String stmt) {
205             tc.countStatement();
206             try (DatabaseConnection.Link l = DatabaseConnection.newLink(true)) {
207                 try (GigiPreparedStatement ps = new GigiPreparedStatement(stmt)) {
208                     ParameterMetaData dt = ps.getParameterMetaData();
209                     int count = dt.getParameterCount();
210                     for (int i = 1; i <= types.length; i++) {
211                         if (i > count) {
212                             if (types[i - 1] != null) {
213                                 errMsg(stmt, "too many params");
214                                 return;
215                             }
216                             continue;
217                         }
218                         int tp = dt.getParameterType(i);
219                         TypeInstantiation t = types[i - 1];
220                         if (t == null) {
221                             errMsg(stmt, "arg " + i + " not set");
222                             return;
223                         }
224                         if ( !t.type.isOfSQLType(tp)) {
225                             errMsg(stmt, "type mismatch. From parameter setting code: " + t + ", in SQL statement: " + tp);
226                             return;
227                         }
228                     }
229                 } catch (SQLException e) {
230                     errMsg(stmt, "SQL exception occurred, probably a syntax error in the SQL statement. See exception for more details.");
231                     throw new Error(e);
232                 }
233             } catch (InterruptedException e) {
234                 e.printStackTrace();
235             }
236         }
237
238         private void errMsg(String stmt, String errMsg) {
239             System.err.println(getPosition());
240             System.err.println("Problem with statement: " + stmt);
241             System.err.println(Arrays.toString(types));
242             System.err.println(errMsg);
243             tc.hadError();
244         }
245
246         public void check() {
247             for (String q : getQuery()) {
248                 check(q);
249             }
250
251         }
252     }
253
254     public SQLTestingVisitor(CompilationUnitDeclaration pu, TranslationCollector tc) {
255         this.pu = pu;
256         this.tc = tc;
257     }
258
259     Deque<SQLOccurrence> ts = new LinkedBlockingDeque<>();
260
261     @Override
262     public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
263         return true;
264     }
265
266     @Override
267     public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
268         return true;
269     }
270
271     @Override
272     public boolean visit(TryStatement tryStatement, BlockScope scope) {
273         ts.push(new SQLOccurrence(tryStatement));
274         return true;
275     }
276
277     @Override
278     public void endVisit(TryStatement tryStatement, BlockScope scope) {
279         SQLOccurrence occ = ts.pop();
280         if (occ.isQuery()) {
281             occ.check();
282         }
283         if (occ.getTarget() != tryStatement) {
284             throw new Error();
285         }
286     }
287
288     @Override
289     public boolean visit(AllocationExpression ae, BlockScope scope) {
290         MethodBinding mb = ae.binding;
291         if (new String(mb.declaringClass.qualifiedPackageName()).equals("club.wpia.gigi.database") && new String(mb.declaringClass.qualifiedSourceName()).equals("GigiPreparedStatement")) {
292             String sig = new String(mb.readableName());
293             if (sig.equals("GigiPreparedStatement(String)") || sig.equals("GigiPreparedStatement(String, boolean)")) {
294                 List<String> l = getQueries(ae.arguments[0], scope);
295                 if (l.size() == 0) {
296                     return false;
297                 }
298                 LinkedList<String> qs = new LinkedList<>();
299                 for (String q : l) {
300                     qs.add(DatabaseConnection.preprocessQuery(q));
301                 }
302                 ts.peek().setQuery(qs, scope.compilationUnitScope().referenceContext.compilationResult, ae.sourceStart);
303             } else {
304                 throw new Error(sig);
305             }
306         }
307         return true;
308     }
309
310     private List<String> getQueries(Expression q, BlockScope scope) {
311         SourceTypeBinding typ = scope.enclosingSourceType();
312         String fullType = new String(typ.qualifiedPackageName()) + "." + new String(typ.qualifiedSourceName());
313         if (fullType.equals("club.wpia.gigi.database.IntegrityVerifier")) {
314             return Arrays.asList();
315         }
316         if (q instanceof StringLiteral) {
317             String s = new String(((StringLiteral) q).source());
318             return Arrays.asList(s);
319         } else if (q instanceof ExtendedStringLiteral) {
320             throw new Error();
321         } else if (q instanceof BinaryExpression) {
322             Expression l = ((BinaryExpression) q).left;
323             Expression r = ((BinaryExpression) q).right;
324             if ( !((BinaryExpression) q).operatorToString().equals("+")) {
325                 throw new Error(((BinaryExpression) q).operatorToString());
326             }
327             List<String> left = getQueries(l, scope);
328             List<String> right = getQueries(r, scope);
329             LinkedList<String> res = new LinkedList<>();
330             for (String leftS : left) {
331                 for (String rightS : right) {
332                     res.add(leftS + rightS);
333                 }
334             }
335             return res;
336         } else if (q instanceof ConditionalExpression) {
337             Expression t = ((ConditionalExpression) q).valueIfTrue;
338             Expression f = ((ConditionalExpression) q).valueIfFalse;
339             List<String> res = new LinkedList<>();
340             res.addAll(getQueries(t, scope));
341             res.addAll(getQueries(f, scope));
342             return res;
343         } else if (q instanceof SingleNameReference) {
344             SingleNameReference ref = (SingleNameReference) q;
345             Constant c = ref.constant;
346             if (c.equals(Constant.NotAConstant)) {
347                 throw new Error(q.toString());
348             }
349             return Arrays.asList(c.stringValue());
350         } else {
351             System.err.println(q.getClass() + ";" + q.toString());
352             throw new Error(q.toString());
353         }
354     }
355
356     @Override
357     public boolean visit(MessageSend messageSend, BlockScope scope) {
358         Expression r = messageSend.receiver;
359         String rec = new String(r.resolvedType.readableName());
360         if (rec.equals("club.wpia.gigi.database.GigiPreparedStatement")) {
361             String selector = new String(messageSend.selector);
362             if (selector.startsWith("set")) {
363                 SQLOccurrence peek = ts.peek();
364                 if (peek == null) {
365                     throw new Error("setting parameter at bad location");
366                 }
367                 IntLiteral i = (IntLiteral) messageSend.arguments[0];
368                 int val = i.constant.intValue();
369                 TypeInstantiation typeInstantiation = getTypeInstantiation(messageSend, selector);
370                 if (peek.types[val - 1] != null && !peek.types[val - 1].equals(typeInstantiation)) {
371                     throw new Error("multiple different typeInstantiations");
372                 }
373                 peek.types[val - 1] = typeInstantiation;
374             }
375         }
376         return true;
377     }
378
379     private TypeInstantiation getTypeInstantiation(MessageSend messageSend, String selector) throws Error {
380         switch (selector) {
381         case "setTimestamp":
382             return new TypeInstantiation(Type.TIMESTAMP);
383         case "setEnum":
384             TypeBinding rn = messageSend.arguments[1].resolvedType;
385             String sn = new String(rn.qualifiedSourceName());
386             String enumClass = new String(rn.readableName());
387             enumClass = enumClass.substring(0, enumClass.length() - sn.length()) + sn.replace('.', '$');
388             String dbn;
389             try {
390                 dbn = ((DBEnum) ((Object[]) Class.forName(enumClass).getMethod("values").invoke(null))[0]).getDBName();
391             } catch (ReflectiveOperationException e) {
392                 throw new Error(e);
393             }
394             return new TypeInstantiation(Type.ENUM, dbn);
395         case "setString":
396             return new TypeInstantiation(Type.STRING);
397         case "setDate":
398             return new TypeInstantiation(Type.DATE);
399         case "setInt":
400             return new TypeInstantiation(Type.INTEGER);
401         case "setBoolean":
402             return new TypeInstantiation(Type.BOOLEAN);
403         default:
404             return new TypeInstantiation(Type.OTHER);
405         }
406     }
407 }