1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.bnf.context; |
7 | |
8 | import java.util.HashMap; |
9 | import java.util.HashSet; |
10 | |
11 | import org.h2.bnf.Bnf; |
12 | import org.h2.bnf.BnfVisitor; |
13 | import org.h2.bnf.Rule; |
14 | import org.h2.bnf.RuleElement; |
15 | import org.h2.bnf.RuleHead; |
16 | import org.h2.bnf.RuleList; |
17 | import org.h2.bnf.Sentence; |
18 | import org.h2.command.Parser; |
19 | import org.h2.message.DbException; |
20 | import org.h2.util.StringUtils; |
21 | |
22 | /** |
23 | * A BNF terminal rule that is linked to the database context information. |
24 | * This class is used by the H2 Console, to support auto-complete. |
25 | */ |
26 | public class DbContextRule implements Rule { |
27 | |
28 | public static final int COLUMN = 0, TABLE = 1, TABLE_ALIAS = 2; |
29 | public static final int NEW_TABLE_ALIAS = 3; |
30 | public static final int COLUMN_ALIAS = 4, SCHEMA = 5, PROCEDURE = 6; |
31 | |
32 | private final DbContents contents; |
33 | private final int type; |
34 | |
35 | private String columnType; |
36 | |
37 | /** |
38 | * BNF terminal rule Constructor |
39 | * @param contents Extract rule from this component |
40 | * @param type Rule type, one of |
41 | * {@link DbContextRule#COLUMN}, |
42 | * {@link DbContextRule#TABLE}, |
43 | * {@link DbContextRule#TABLE_ALIAS}, |
44 | * {@link DbContextRule#NEW_TABLE_ALIAS}, |
45 | * {@link DbContextRule#COLUMN_ALIAS}, |
46 | * {@link DbContextRule#SCHEMA} |
47 | */ |
48 | public DbContextRule(DbContents contents, int type) { |
49 | this.contents = contents; |
50 | this.type = type; |
51 | } |
52 | |
53 | /** |
54 | * @param columnType COLUMN Auto completion can be filtered by column type |
55 | */ |
56 | public void setColumnType(String columnType) { |
57 | this.columnType = columnType; |
58 | } |
59 | |
60 | @Override |
61 | public void setLinks(HashMap<String, RuleHead> ruleMap) { |
62 | // nothing to do |
63 | } |
64 | |
65 | @Override |
66 | public void accept(BnfVisitor visitor) { |
67 | // nothing to do |
68 | } |
69 | |
70 | @Override |
71 | public boolean autoComplete(Sentence sentence) { |
72 | String query = sentence.getQuery(), s = query; |
73 | String up = sentence.getQueryUpper(); |
74 | switch (type) { |
75 | case SCHEMA: { |
76 | DbSchema[] schemas = contents.getSchemas(); |
77 | String best = null; |
78 | DbSchema bestSchema = null; |
79 | for (DbSchema schema: schemas) { |
80 | String name = StringUtils.toUpperEnglish(schema.name); |
81 | if (up.startsWith(name)) { |
82 | if (best == null || name.length() > best.length()) { |
83 | best = name; |
84 | bestSchema = schema; |
85 | } |
86 | } else if (s.length() == 0 || name.startsWith(up)) { |
87 | if (s.length() < name.length()) { |
88 | sentence.add(name, name.substring(s.length()), type); |
89 | sentence.add(schema.quotedName + ".", |
90 | schema.quotedName.substring(s.length()) + ".", |
91 | Sentence.CONTEXT); |
92 | } |
93 | } |
94 | } |
95 | if (best != null) { |
96 | sentence.setLastMatchedSchema(bestSchema); |
97 | s = s.substring(best.length()); |
98 | } |
99 | break; |
100 | } |
101 | case TABLE: { |
102 | DbSchema schema = sentence.getLastMatchedSchema(); |
103 | if (schema == null) { |
104 | schema = contents.getDefaultSchema(); |
105 | } |
106 | DbTableOrView[] tables = schema.getTables(); |
107 | String best = null; |
108 | DbTableOrView bestTable = null; |
109 | for (DbTableOrView table : tables) { |
110 | String compare = up; |
111 | String name = StringUtils.toUpperEnglish(table.getName()); |
112 | if (table.getQuotedName().length() > name.length()) { |
113 | name = table.getQuotedName(); |
114 | compare = query; |
115 | } |
116 | if (compare.startsWith(name)) { |
117 | if (best == null || name.length() > best.length()) { |
118 | best = name; |
119 | bestTable = table; |
120 | } |
121 | } else if (s.length() == 0 || name.startsWith(compare)) { |
122 | if (s.length() < name.length()) { |
123 | sentence.add(table.getQuotedName(), |
124 | table.getQuotedName().substring(s.length()), |
125 | Sentence.CONTEXT); |
126 | } |
127 | } |
128 | } |
129 | if (best != null) { |
130 | sentence.setLastMatchedTable(bestTable); |
131 | sentence.addTable(bestTable); |
132 | s = s.substring(best.length()); |
133 | } |
134 | break; |
135 | } |
136 | case NEW_TABLE_ALIAS: |
137 | s = autoCompleteTableAlias(sentence, true); |
138 | break; |
139 | case TABLE_ALIAS: |
140 | s = autoCompleteTableAlias(sentence, false); |
141 | break; |
142 | case COLUMN_ALIAS: { |
143 | int i = 0; |
144 | if (query.indexOf(' ') < 0) { |
145 | break; |
146 | } |
147 | for (; i < up.length(); i++) { |
148 | char ch = up.charAt(i); |
149 | if (ch != '_' && !Character.isLetterOrDigit(ch)) { |
150 | break; |
151 | } |
152 | } |
153 | if (i == 0) { |
154 | break; |
155 | } |
156 | String alias = up.substring(0, i); |
157 | if (Parser.isKeyword(alias, true)) { |
158 | break; |
159 | } |
160 | s = s.substring(alias.length()); |
161 | break; |
162 | } |
163 | case COLUMN: { |
164 | HashSet<DbTableOrView> set = sentence.getTables(); |
165 | String best = null; |
166 | DbTableOrView last = sentence.getLastMatchedTable(); |
167 | if (last != null && last.getColumns() != null) { |
168 | for (DbColumn column : last.getColumns()) { |
169 | String compare = up; |
170 | String name = StringUtils.toUpperEnglish(column.getName()); |
171 | if (column.getQuotedName().length() > name.length()) { |
172 | name = column.getQuotedName(); |
173 | compare = query; |
174 | } |
175 | if (compare.startsWith(name) && |
176 | (columnType == null || |
177 | column.getDataType().contains(columnType))) { |
178 | String b = s.substring(name.length()); |
179 | if (best == null || b.length() < best.length()) { |
180 | best = b; |
181 | } else if (s.length() == 0 || name.startsWith(compare)) { |
182 | if (s.length() < name.length()) { |
183 | sentence.add(column.getName(), |
184 | column.getName().substring(s.length()), |
185 | Sentence.CONTEXT); |
186 | } |
187 | } |
188 | } |
189 | } |
190 | } |
191 | for (DbSchema schema : contents.getSchemas()) { |
192 | for (DbTableOrView table : schema.getTables()) { |
193 | if (table != last && set != null && !set.contains(table)) { |
194 | continue; |
195 | } |
196 | if (table == null || table.getColumns() == null) { |
197 | continue; |
198 | } |
199 | for (DbColumn column : table.getColumns()) { |
200 | String name = StringUtils.toUpperEnglish(column |
201 | .getName()); |
202 | if (columnType == null |
203 | || column.getDataType().contains(columnType)) { |
204 | if (up.startsWith(name)) { |
205 | String b = s.substring(name.length()); |
206 | if (best == null || b.length() < best.length()) { |
207 | best = b; |
208 | } |
209 | } else if (s.length() == 0 || name.startsWith(up)) { |
210 | if (s.length() < name.length()) { |
211 | sentence.add(column.getName(), |
212 | column.getName().substring(s.length()), |
213 | Sentence.CONTEXT); |
214 | } |
215 | } |
216 | } |
217 | } |
218 | } |
219 | } |
220 | if (best != null) { |
221 | s = best; |
222 | } |
223 | break; |
224 | } |
225 | case PROCEDURE: |
226 | autoCompleteProcedure(sentence); |
227 | break; |
228 | default: |
229 | throw DbException.throwInternalError("type=" + type); |
230 | } |
231 | if (!s.equals(query)) { |
232 | while (Bnf.startWithSpace(s)) { |
233 | s = s.substring(1); |
234 | } |
235 | sentence.setQuery(s); |
236 | return true; |
237 | } |
238 | return false; |
239 | } |
240 | private void autoCompleteProcedure(Sentence sentence) { |
241 | DbSchema schema = sentence.getLastMatchedSchema(); |
242 | if (schema == null) { |
243 | schema = contents.getDefaultSchema(); |
244 | } |
245 | String incompleteSentence = sentence.getQueryUpper(); |
246 | String incompleteFunctionName = incompleteSentence; |
247 | if (incompleteSentence.contains("(")) { |
248 | incompleteFunctionName = incompleteSentence.substring(0, |
249 | incompleteSentence.indexOf('(')).trim(); |
250 | } |
251 | |
252 | // Common elements |
253 | RuleElement openBracket = new RuleElement("(", "Function"); |
254 | RuleElement closeBracket = new RuleElement(")", "Function"); |
255 | RuleElement comma = new RuleElement(",", "Function"); |
256 | |
257 | // Fetch all elements |
258 | for (DbProcedure procedure : schema.getProcedures()) { |
259 | final String procName = procedure.getName(); |
260 | if (procName.startsWith(incompleteFunctionName)) { |
261 | // That's it, build a RuleList from this function |
262 | RuleElement procedureElement = new RuleElement(procName, |
263 | "Function"); |
264 | RuleList rl = new RuleList(procedureElement, openBracket, false); |
265 | // Go further only if the user use open bracket |
266 | if (incompleteSentence.contains("(")) { |
267 | for (DbColumn parameter : procedure.getParameters()) { |
268 | if (parameter.getPosition() > 1) { |
269 | rl = new RuleList(rl, comma, false); |
270 | } |
271 | DbContextRule columnRule = new DbContextRule(contents, |
272 | COLUMN); |
273 | String parameterType = parameter.getDataType(); |
274 | // Remove precision |
275 | if (parameterType.contains("(")) { |
276 | parameterType = parameterType.substring(0, |
277 | parameterType.indexOf('(')); |
278 | } |
279 | columnRule.setColumnType(parameterType); |
280 | rl = new RuleList(rl, columnRule, false); |
281 | } |
282 | rl = new RuleList(rl, closeBracket , false); |
283 | } |
284 | rl.autoComplete(sentence); |
285 | } |
286 | } |
287 | } |
288 | |
289 | private static String autoCompleteTableAlias(Sentence sentence, |
290 | boolean newAlias) { |
291 | String s = sentence.getQuery(); |
292 | String up = sentence.getQueryUpper(); |
293 | int i = 0; |
294 | for (; i < up.length(); i++) { |
295 | char ch = up.charAt(i); |
296 | if (ch != '_' && !Character.isLetterOrDigit(ch)) { |
297 | break; |
298 | } |
299 | } |
300 | if (i == 0) { |
301 | return s; |
302 | } |
303 | String alias = up.substring(0, i); |
304 | if ("SET".equals(alias) || Parser.isKeyword(alias, true)) { |
305 | return s; |
306 | } |
307 | if (newAlias) { |
308 | sentence.addAlias(alias, sentence.getLastTable()); |
309 | } |
310 | HashMap<String, DbTableOrView> map = sentence.getAliases(); |
311 | if ((map != null && map.containsKey(alias)) || |
312 | (sentence.getLastTable() == null)) { |
313 | if (newAlias && s.length() == alias.length()) { |
314 | return s; |
315 | } |
316 | s = s.substring(alias.length()); |
317 | if (s.length() == 0) { |
318 | sentence.add(alias + ".", ".", Sentence.CONTEXT); |
319 | } |
320 | return s; |
321 | } |
322 | HashSet<DbTableOrView> tables = sentence.getTables(); |
323 | if (tables != null) { |
324 | String best = null; |
325 | for (DbTableOrView table : tables) { |
326 | String tableName = |
327 | StringUtils.toUpperEnglish(table.getName()); |
328 | if (alias.startsWith(tableName) && |
329 | (best == null || tableName.length() > best.length())) { |
330 | sentence.setLastMatchedTable(table); |
331 | best = tableName; |
332 | } else if (s.length() == 0 || tableName.startsWith(alias)) { |
333 | sentence.add(tableName + ".", |
334 | tableName.substring(s.length()) + ".", |
335 | Sentence.CONTEXT); |
336 | } |
337 | } |
338 | if (best != null) { |
339 | s = s.substring(best.length()); |
340 | if (s.length() == 0) { |
341 | sentence.add(alias + ".", ".", Sentence.CONTEXT); |
342 | } |
343 | return s; |
344 | } |
345 | } |
346 | return s; |
347 | } |
348 | |
349 | } |