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.engine; |
7 | |
8 | import java.lang.reflect.Array; |
9 | import java.lang.reflect.InvocationTargetException; |
10 | import java.lang.reflect.Method; |
11 | import java.lang.reflect.Modifier; |
12 | import java.sql.Connection; |
13 | import java.util.ArrayList; |
14 | import java.util.Arrays; |
15 | |
16 | import org.h2.Driver; |
17 | import org.h2.api.ErrorCode; |
18 | import org.h2.command.Parser; |
19 | import org.h2.expression.Expression; |
20 | import org.h2.message.DbException; |
21 | import org.h2.message.Trace; |
22 | import org.h2.schema.Schema; |
23 | import org.h2.schema.SchemaObjectBase; |
24 | import org.h2.table.Table; |
25 | import org.h2.util.JdbcUtils; |
26 | import org.h2.util.New; |
27 | import org.h2.util.SourceCompiler; |
28 | import org.h2.util.StatementBuilder; |
29 | import org.h2.util.StringUtils; |
30 | import org.h2.value.DataType; |
31 | import org.h2.value.Value; |
32 | import org.h2.value.ValueArray; |
33 | import org.h2.value.ValueNull; |
34 | |
35 | /** |
36 | * Represents a user-defined function, or alias. |
37 | * |
38 | * @author Thomas Mueller |
39 | * @author Gary Tong |
40 | */ |
41 | public class FunctionAlias extends SchemaObjectBase { |
42 | |
43 | private String className; |
44 | private String methodName; |
45 | private String source; |
46 | private JavaMethod[] javaMethods; |
47 | private boolean deterministic; |
48 | private boolean bufferResultSetToLocalTemp = true; |
49 | |
50 | private FunctionAlias(Schema schema, int id, String name) { |
51 | initSchemaObjectBase(schema, id, name, Trace.FUNCTION); |
52 | } |
53 | |
54 | /** |
55 | * Create a new alias based on a method name. |
56 | * |
57 | * @param schema the schema |
58 | * @param id the id |
59 | * @param name the name |
60 | * @param javaClassMethod the class and method name |
61 | * @param force create the object even if the class or method does not exist |
62 | * @param bufferResultSetToLocalTemp whether the result should be buffered |
63 | * @return the database object |
64 | */ |
65 | public static FunctionAlias newInstance( |
66 | Schema schema, int id, String name, String javaClassMethod, |
67 | boolean force, boolean bufferResultSetToLocalTemp) { |
68 | FunctionAlias alias = new FunctionAlias(schema, id, name); |
69 | int paren = javaClassMethod.indexOf('('); |
70 | int lastDot = javaClassMethod.lastIndexOf('.', paren < 0 ? |
71 | javaClassMethod.length() : paren); |
72 | if (lastDot < 0) { |
73 | throw DbException.get(ErrorCode.SYNTAX_ERROR_1, javaClassMethod); |
74 | } |
75 | alias.className = javaClassMethod.substring(0, lastDot); |
76 | alias.methodName = javaClassMethod.substring(lastDot + 1); |
77 | alias.bufferResultSetToLocalTemp = bufferResultSetToLocalTemp; |
78 | alias.init(force); |
79 | return alias; |
80 | } |
81 | |
82 | /** |
83 | * Create a new alias based on source code. |
84 | * |
85 | * @param schema the schema |
86 | * @param id the id |
87 | * @param name the name |
88 | * @param source the source code |
89 | * @param force create the object even if the class or method does not exist |
90 | * @param bufferResultSetToLocalTemp whether the result should be buffered |
91 | * @return the database object |
92 | */ |
93 | public static FunctionAlias newInstanceFromSource( |
94 | Schema schema, int id, String name, String source, boolean force, |
95 | boolean bufferResultSetToLocalTemp) { |
96 | FunctionAlias alias = new FunctionAlias(schema, id, name); |
97 | alias.source = source; |
98 | alias.bufferResultSetToLocalTemp = bufferResultSetToLocalTemp; |
99 | alias.init(force); |
100 | return alias; |
101 | } |
102 | |
103 | private void init(boolean force) { |
104 | try { |
105 | // at least try to compile the class, otherwise the data type is not |
106 | // initialized if it could be |
107 | load(); |
108 | } catch (DbException e) { |
109 | if (!force) { |
110 | throw e; |
111 | } |
112 | } |
113 | } |
114 | |
115 | private synchronized void load() { |
116 | if (javaMethods != null) { |
117 | return; |
118 | } |
119 | if (source != null) { |
120 | loadFromSource(); |
121 | } else { |
122 | loadClass(); |
123 | } |
124 | } |
125 | |
126 | private void loadFromSource() { |
127 | SourceCompiler compiler = database.getCompiler(); |
128 | synchronized (compiler) { |
129 | String fullClassName = Constants.USER_PACKAGE + "." + getName(); |
130 | compiler.setSource(fullClassName, source); |
131 | try { |
132 | Method m = compiler.getMethod(fullClassName); |
133 | JavaMethod method = new JavaMethod(m, 0); |
134 | javaMethods = new JavaMethod[] { |
135 | method |
136 | }; |
137 | } catch (DbException e) { |
138 | throw e; |
139 | } catch (Exception e) { |
140 | throw DbException.get(ErrorCode.SYNTAX_ERROR_1, e, source); |
141 | } |
142 | } |
143 | } |
144 | |
145 | private void loadClass() { |
146 | Class<?> javaClass = JdbcUtils.loadUserClass(className); |
147 | Method[] methods = javaClass.getMethods(); |
148 | ArrayList<JavaMethod> list = New.arrayList(); |
149 | for (int i = 0, len = methods.length; i < len; i++) { |
150 | Method m = methods[i]; |
151 | if (!Modifier.isStatic(m.getModifiers())) { |
152 | continue; |
153 | } |
154 | if (m.getName().equals(methodName) || |
155 | getMethodSignature(m).equals(methodName)) { |
156 | JavaMethod javaMethod = new JavaMethod(m, i); |
157 | for (JavaMethod old : list) { |
158 | if (old.getParameterCount() == javaMethod.getParameterCount()) { |
159 | throw DbException.get(ErrorCode. |
160 | METHODS_MUST_HAVE_DIFFERENT_PARAMETER_COUNTS_2, |
161 | old.toString(), javaMethod.toString()); |
162 | } |
163 | } |
164 | list.add(javaMethod); |
165 | } |
166 | } |
167 | if (list.size() == 0) { |
168 | throw DbException.get( |
169 | ErrorCode.PUBLIC_STATIC_JAVA_METHOD_NOT_FOUND_1, |
170 | methodName + " (" + className + ")"); |
171 | } |
172 | javaMethods = new JavaMethod[list.size()]; |
173 | list.toArray(javaMethods); |
174 | // Sort elements. Methods with a variable number of arguments must be at |
175 | // the end. Reason: there could be one method without parameters and one |
176 | // with a variable number. The one without parameters needs to be used |
177 | // if no parameters are given. |
178 | Arrays.sort(javaMethods); |
179 | } |
180 | |
181 | private static String getMethodSignature(Method m) { |
182 | StatementBuilder buff = new StatementBuilder(m.getName()); |
183 | buff.append('('); |
184 | for (Class<?> p : m.getParameterTypes()) { |
185 | // do not use a space here, because spaces are removed |
186 | // in CreateFunctionAlias.setJavaClassMethod() |
187 | buff.appendExceptFirst(","); |
188 | if (p.isArray()) { |
189 | buff.append(p.getComponentType().getName()).append("[]"); |
190 | } else { |
191 | buff.append(p.getName()); |
192 | } |
193 | } |
194 | return buff.append(')').toString(); |
195 | } |
196 | |
197 | @Override |
198 | public String getCreateSQLForCopy(Table table, String quotedName) { |
199 | throw DbException.throwInternalError(); |
200 | } |
201 | |
202 | @Override |
203 | public String getDropSQL() { |
204 | return "DROP ALIAS IF EXISTS " + getSQL(); |
205 | } |
206 | |
207 | @Override |
208 | public String getSQL() { |
209 | // TODO can remove this method once FUNCTIONS_IN_SCHEMA is enabled |
210 | if (database.getSettings().functionsInSchema || |
211 | !getSchema().getName().equals(Constants.SCHEMA_MAIN)) { |
212 | return super.getSQL(); |
213 | } |
214 | return Parser.quoteIdentifier(getName()); |
215 | } |
216 | |
217 | @Override |
218 | public String getCreateSQL() { |
219 | StringBuilder buff = new StringBuilder("CREATE FORCE ALIAS "); |
220 | buff.append(getSQL()); |
221 | if (deterministic) { |
222 | buff.append(" DETERMINISTIC"); |
223 | } |
224 | if (!bufferResultSetToLocalTemp) { |
225 | buff.append(" NOBUFFER"); |
226 | } |
227 | if (source != null) { |
228 | buff.append(" AS ").append(StringUtils.quoteStringSQL(source)); |
229 | } else { |
230 | buff.append(" FOR ").append(Parser.quoteIdentifier( |
231 | className + "." + methodName)); |
232 | } |
233 | return buff.toString(); |
234 | } |
235 | |
236 | @Override |
237 | public int getType() { |
238 | return DbObject.FUNCTION_ALIAS; |
239 | } |
240 | |
241 | @Override |
242 | public synchronized void removeChildrenAndResources(Session session) { |
243 | database.removeMeta(session, getId()); |
244 | className = null; |
245 | methodName = null; |
246 | javaMethods = null; |
247 | invalidate(); |
248 | } |
249 | |
250 | @Override |
251 | public void checkRename() { |
252 | throw DbException.getUnsupportedException("RENAME"); |
253 | } |
254 | |
255 | /** |
256 | * Find the Java method that matches the arguments. |
257 | * |
258 | * @param args the argument list |
259 | * @return the Java method |
260 | * @throws DbException if no matching method could be found |
261 | */ |
262 | public JavaMethod findJavaMethod(Expression[] args) { |
263 | load(); |
264 | int parameterCount = args.length; |
265 | for (JavaMethod m : javaMethods) { |
266 | int count = m.getParameterCount(); |
267 | if (count == parameterCount || (m.isVarArgs() && |
268 | count <= parameterCount + 1)) { |
269 | return m; |
270 | } |
271 | } |
272 | throw DbException.get(ErrorCode.METHOD_NOT_FOUND_1, getName() + " (" + |
273 | className + ", parameter count: " + parameterCount + ")"); |
274 | } |
275 | |
276 | public String getJavaClassName() { |
277 | return this.className; |
278 | } |
279 | |
280 | public String getJavaMethodName() { |
281 | return this.methodName; |
282 | } |
283 | |
284 | /** |
285 | * Get the Java methods mapped by this function. |
286 | * |
287 | * @return the Java methods. |
288 | */ |
289 | public JavaMethod[] getJavaMethods() { |
290 | load(); |
291 | return javaMethods; |
292 | } |
293 | |
294 | public void setDeterministic(boolean deterministic) { |
295 | this.deterministic = deterministic; |
296 | } |
297 | |
298 | public boolean isDeterministic() { |
299 | return deterministic; |
300 | } |
301 | |
302 | public String getSource() { |
303 | return source; |
304 | } |
305 | |
306 | /** |
307 | * Checks if the given method takes a variable number of arguments. For Java |
308 | * 1.4 and older, false is returned. Example: |
309 | * <pre> |
310 | * public static double mean(double... values) |
311 | * </pre> |
312 | * |
313 | * @param m the method to test |
314 | * @return true if the method takes a variable number of arguments. |
315 | */ |
316 | static boolean isVarArgs(Method m) { |
317 | if ("1.5".compareTo(SysProperties.JAVA_SPECIFICATION_VERSION) > 0) { |
318 | return false; |
319 | } |
320 | try { |
321 | Method isVarArgs = m.getClass().getMethod("isVarArgs"); |
322 | Boolean result = (Boolean) isVarArgs.invoke(m); |
323 | return result.booleanValue(); |
324 | } catch (Exception e) { |
325 | return false; |
326 | } |
327 | } |
328 | |
329 | /** |
330 | * Should the return value ResultSet be buffered in a local temporary file? |
331 | * |
332 | * @return true if yes |
333 | */ |
334 | public boolean isBufferResultSetToLocalTemp() { |
335 | return bufferResultSetToLocalTemp; |
336 | } |
337 | |
338 | /** |
339 | * There may be multiple Java methods that match a function name. |
340 | * Each method must have a different number of parameters however. |
341 | * This helper class represents one such method. |
342 | */ |
343 | public static class JavaMethod implements Comparable<JavaMethod> { |
344 | private final int id; |
345 | private final Method method; |
346 | private final int dataType; |
347 | private boolean hasConnectionParam; |
348 | private boolean varArgs; |
349 | private Class<?> varArgClass; |
350 | private int paramCount; |
351 | |
352 | JavaMethod(Method method, int id) { |
353 | this.method = method; |
354 | this.id = id; |
355 | Class<?>[] paramClasses = method.getParameterTypes(); |
356 | paramCount = paramClasses.length; |
357 | if (paramCount > 0) { |
358 | Class<?> paramClass = paramClasses[0]; |
359 | if (Connection.class.isAssignableFrom(paramClass)) { |
360 | hasConnectionParam = true; |
361 | paramCount--; |
362 | } |
363 | } |
364 | if (paramCount > 0) { |
365 | Class<?> lastArg = paramClasses[paramClasses.length - 1]; |
366 | if (lastArg.isArray() && FunctionAlias.isVarArgs(method)) { |
367 | varArgs = true; |
368 | varArgClass = lastArg.getComponentType(); |
369 | } |
370 | } |
371 | Class<?> returnClass = method.getReturnType(); |
372 | dataType = DataType.getTypeFromClass(returnClass); |
373 | } |
374 | |
375 | @Override |
376 | public String toString() { |
377 | return method.toString(); |
378 | } |
379 | |
380 | /** |
381 | * Check if this function requires a database connection. |
382 | * |
383 | * @return if the function requires a connection |
384 | */ |
385 | public boolean hasConnectionParam() { |
386 | return this.hasConnectionParam; |
387 | } |
388 | |
389 | /** |
390 | * Call the user-defined function and return the value. |
391 | * |
392 | * @param session the session |
393 | * @param args the argument list |
394 | * @param columnList true if the function should only return the column |
395 | * list |
396 | * @return the value |
397 | */ |
398 | public Value getValue(Session session, Expression[] args, |
399 | boolean columnList) { |
400 | Class<?>[] paramClasses = method.getParameterTypes(); |
401 | Object[] params = new Object[paramClasses.length]; |
402 | int p = 0; |
403 | if (hasConnectionParam && params.length > 0) { |
404 | params[p++] = session.createConnection(columnList); |
405 | } |
406 | |
407 | // allocate array for varArgs parameters |
408 | Object varArg = null; |
409 | if (varArgs) { |
410 | int len = args.length - params.length + 1 + |
411 | (hasConnectionParam ? 1 : 0); |
412 | varArg = Array.newInstance(varArgClass, len); |
413 | params[params.length - 1] = varArg; |
414 | } |
415 | |
416 | for (int a = 0, len = args.length; a < len; a++, p++) { |
417 | boolean currentIsVarArg = varArgs && |
418 | p >= paramClasses.length - 1; |
419 | Class<?> paramClass; |
420 | if (currentIsVarArg) { |
421 | paramClass = varArgClass; |
422 | } else { |
423 | paramClass = paramClasses[p]; |
424 | } |
425 | int type = DataType.getTypeFromClass(paramClass); |
426 | Value v = args[a].getValue(session); |
427 | Object o; |
428 | if (Value.class.isAssignableFrom(paramClass)) { |
429 | o = v; |
430 | } else if (v.getType() == Value.ARRAY && |
431 | paramClass.isArray() && |
432 | paramClass.getComponentType() != Object.class) { |
433 | Value[] array = ((ValueArray) v).getList(); |
434 | Object[] objArray = (Object[]) Array.newInstance( |
435 | paramClass.getComponentType(), array.length); |
436 | int componentType = DataType.getTypeFromClass( |
437 | paramClass.getComponentType()); |
438 | for (int i = 0; i < objArray.length; i++) { |
439 | objArray[i] = array[i].convertTo(componentType).getObject(); |
440 | } |
441 | o = objArray; |
442 | } else { |
443 | v = v.convertTo(type); |
444 | o = v.getObject(); |
445 | } |
446 | if (o == null) { |
447 | if (paramClass.isPrimitive()) { |
448 | if (columnList) { |
449 | // If the column list is requested, the parameters |
450 | // may be null. Need to set to default value, |
451 | // otherwise the function can't be called at all. |
452 | o = DataType.getDefaultForPrimitiveType(paramClass); |
453 | } else { |
454 | // NULL for a java primitive: return NULL |
455 | return ValueNull.INSTANCE; |
456 | } |
457 | } |
458 | } else { |
459 | if (!paramClass.isAssignableFrom(o.getClass()) && !paramClass.isPrimitive()) { |
460 | o = DataType.convertTo(session.createConnection(false), v, paramClass); |
461 | } |
462 | } |
463 | if (currentIsVarArg) { |
464 | Array.set(varArg, p - params.length + 1, o); |
465 | } else { |
466 | params[p] = o; |
467 | } |
468 | } |
469 | boolean old = session.getAutoCommit(); |
470 | Value identity = session.getLastScopeIdentity(); |
471 | boolean defaultConnection = session.getDatabase(). |
472 | getSettings().defaultConnection; |
473 | try { |
474 | session.setAutoCommit(false); |
475 | Object returnValue; |
476 | try { |
477 | if (defaultConnection) { |
478 | Driver.setDefaultConnection( |
479 | session.createConnection(columnList)); |
480 | } |
481 | returnValue = method.invoke(null, params); |
482 | if (returnValue == null) { |
483 | return ValueNull.INSTANCE; |
484 | } |
485 | } catch (InvocationTargetException e) { |
486 | StatementBuilder buff = new StatementBuilder(method.getName()); |
487 | buff.append('('); |
488 | for (Object o : params) { |
489 | buff.appendExceptFirst(", "); |
490 | buff.append(o == null ? "null" : o.toString()); |
491 | } |
492 | buff.append(')'); |
493 | throw DbException.convertInvocation(e, buff.toString()); |
494 | } catch (Exception e) { |
495 | throw DbException.convert(e); |
496 | } |
497 | if (Value.class.isAssignableFrom(method.getReturnType())) { |
498 | return (Value) returnValue; |
499 | } |
500 | Value ret = DataType.convertToValue(session, returnValue, dataType); |
501 | return ret.convertTo(dataType); |
502 | } finally { |
503 | session.setLastScopeIdentity(identity); |
504 | session.setAutoCommit(old); |
505 | if (defaultConnection) { |
506 | Driver.setDefaultConnection(null); |
507 | } |
508 | } |
509 | } |
510 | |
511 | public Class<?>[] getColumnClasses() { |
512 | return method.getParameterTypes(); |
513 | } |
514 | |
515 | public int getDataType() { |
516 | return dataType; |
517 | } |
518 | |
519 | public int getParameterCount() { |
520 | return paramCount; |
521 | } |
522 | |
523 | public boolean isVarArgs() { |
524 | return varArgs; |
525 | } |
526 | |
527 | @Override |
528 | public int compareTo(JavaMethod m) { |
529 | if (varArgs != m.varArgs) { |
530 | return varArgs ? 1 : -1; |
531 | } |
532 | if (paramCount != m.paramCount) { |
533 | return paramCount - m.paramCount; |
534 | } |
535 | if (hasConnectionParam != m.hasConnectionParam) { |
536 | return hasConnectionParam ? 1 : -1; |
537 | } |
538 | return id - m.id; |
539 | } |
540 | |
541 | } |
542 | |
543 | } |