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