/* * This software is subject to the terms of the Eclipse Public License v1.0 * Agreement, available at the following URL: * http://www.eclipse.org/legal/epl-v10.html. * You must accept the terms of that agreement to use this software. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package mondrian.olap.fun; import mondrian.calc.*; import mondrian.calc.impl.AbstractCalc; import mondrian.calc.impl.GenericCalc; import mondrian.mdx.ResolvedFunCall; import mondrian.olap.*; import java.lang.annotation.*; import java.lang.reflect.*; import java.util.*; /** * MDX function which is implemented by a Java method. When the function is * executed, the method is invoked via reflection. * * @author wgorman, jhyde * @since Jan 5, 2008 */ public class JavaFunDef extends FunDefBase { private static final Map<Class, Integer> mapClazzToCategory = new HashMap<Class, Integer>(); private static final String className = JavaFunDef.class.getName(); static { mapClazzToCategory.put(String.class, Category.String); mapClazzToCategory.put(Double.class, Category.Numeric); mapClazzToCategory.put(double.class, Category.Numeric); mapClazzToCategory.put(Integer.class, Category.Integer); mapClazzToCategory.put(int.class, Category.Integer); mapClazzToCategory.put(boolean.class, Category.Logical); mapClazzToCategory.put(Object.class, Category.Value); mapClazzToCategory.put(Date.class, Category.DateTime); mapClazzToCategory.put(float.class, Category.Numeric); mapClazzToCategory.put(long.class, Category.Numeric); mapClazzToCategory.put(double[].class, Category.Array); mapClazzToCategory.put(char.class, Category.String); mapClazzToCategory.put(byte.class, Category.Integer); } private final Method method; /** * Creates a JavaFunDef. * * @param name Name * @param desc Description * @param syntax Syntax * @param returnCategory Return type * @param paramCategories Parameter types * @param method Java method which implements this function */ public JavaFunDef( String name, String desc, Syntax syntax, int returnCategory, int[] paramCategories, Method method) { super(name, null, desc, syntax, returnCategory, paramCategories); this.method = method; } public Calc compileCall( ResolvedFunCall call, ExpCompiler compiler) { final Calc[] calcs = new Calc[parameterCategories.length]; final Class<?>[] parameterTypes = method.getParameterTypes(); for (int i = 0; i < calcs.length;i++) { calcs[i] = compileTo( compiler, call.getArgs()[i], parameterTypes[i]); } return new JavaMethodCalc(call, calcs, method); } private static int getCategory(Class clazz) { return mapClazzToCategory.get(clazz); } private static int getReturnCategory(Method m) { return getCategory(m.getReturnType()); } private static int[] getParameterCategories(Method m) { int arr[] = new int[m.getParameterTypes().length]; for (int i = 0; i < m.getParameterTypes().length; i++) { arr[i] = getCategory(m.getParameterTypes()[i]); } return arr; } private static FunDef generateFunDef(final Method method) { String name = getAnnotation( method, className + "$FunctionName", method.getName()); String desc = getAnnotation( method, className + "$Description", ""); Syntax syntax = getAnnotation( method, className + "$SyntaxDef", Syntax.Function); int returnCategory = getReturnCategory(method); int paramCategories[] = getParameterCategories(method); return new JavaFunDef( name, desc, syntax, returnCategory, paramCategories, method); } /** * Scans a java class and returns a list of function definitions, one for * each static method which is suitable to become an MDX function. * * @param clazz Class * @return List of function definitions */ public static List<FunDef> scan(Class clazz) { List<FunDef> list = new ArrayList<FunDef>(); Method[] methods = clazz.getMethods(); for (Method method : methods) { if (Modifier.isStatic(method.getModifiers()) && !method.getName().equals("main")) { list.add(generateFunDef(method)); } } return list; } /** * Compiles an expression to a calc of the required result type. * * <p>Since the result of evaluating the calc will be passed to the method * using reflection, it is important that the calc returns * <em>precisely</em> the correct type: if a method requires an * <code>int</code>, you can pass an {@link Integer} but not a {@link Long} * or {@link Float}. * * <p>If it can be determined that the underlying calc will never return * null, generates an optimal form with one fewer object instantiation. * * @param compiler Compiler * @param exp Expression to compile * @param clazz Desired class * @return compiled expression */ private static Calc compileTo(ExpCompiler compiler, Exp exp, Class clazz) { if (clazz == String.class) { return compiler.compileString(exp); } else if (clazz == Date.class) { return compiler.compileDateTime(exp); } else if (clazz == boolean.class) { return compiler.compileBoolean(exp); } else if (clazz == byte.class) { final IntegerCalc integerCalc = compiler.compileInteger(exp); if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { // We know that the calculation will never return a null value, // so generate optimized code. return new AbstractCalc2(exp, integerCalc) { public Object evaluate(Evaluator evaluator) { return (byte) integerCalc.evaluateInteger(evaluator); } }; } else { return new AbstractCalc2(exp, integerCalc) { public Object evaluate(Evaluator evaluator) { Integer i = (Integer) integerCalc.evaluate(evaluator); return i == null ? null : (byte) i.intValue(); } }; } } else if (clazz == char.class) { final StringCalc stringCalc = compiler.compileString(exp); return new AbstractCalc2(exp, stringCalc) { public Object evaluate(Evaluator evaluator) { final String string = stringCalc.evaluateString(evaluator); return Character.valueOf( string == null || string.length() < 1 ? (char) 0 : string.charAt(0)); } }; } else if (clazz == short.class) { final IntegerCalc integerCalc = compiler.compileInteger(exp); if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { return new AbstractCalc2(exp, integerCalc) { public Object evaluate(Evaluator evaluator) { return (short) integerCalc.evaluateInteger(evaluator); } }; } else { return new AbstractCalc2(exp, integerCalc) { public Object evaluate(Evaluator evaluator) { Integer i = (Integer) integerCalc.evaluate(evaluator); return i == null ? null : (short) i.intValue(); } }; } } else if (clazz == int.class) { return compiler.compileInteger(exp); } else if (clazz == long.class) { final IntegerCalc integerCalc = compiler.compileInteger(exp); if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { return new AbstractCalc2(exp, integerCalc) { public Object evaluate(Evaluator evaluator) { return (long) integerCalc.evaluateInteger(evaluator); } }; } else { return new AbstractCalc2(exp, integerCalc) { public Object evaluate(Evaluator evaluator) { Integer i = (Integer) integerCalc.evaluate(evaluator); return i == null ? null : (long) i.intValue(); } }; } } else if (clazz == float.class) { final DoubleCalc doubleCalc = compiler.compileDouble(exp); if (doubleCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { return new AbstractCalc2(exp, doubleCalc) { public Object evaluate(Evaluator evaluator) { Double v = (Double) doubleCalc.evaluate(evaluator); return v == null ? null : v.floatValue(); } }; } else { return new AbstractCalc2(exp, doubleCalc) { public Object evaluate(Evaluator evaluator) { return (float) doubleCalc.evaluateDouble(evaluator); } }; } } else if (clazz == double.class) { return compiler.compileDouble(exp); } else if (clazz == Object.class) { return compiler.compileScalar(exp, false); } else { throw newInternal("expected primitive type, got " + clazz); } } /** * Annotation which allows you to tag a Java method with the name of the * MDX function it implements. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface FunctionName { public abstract String value(); } /** * Annotation which allows you to tag a Java method with the description * of the MDX function it implements. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Description { public abstract String value(); } /** * Annotation which allows you to tag a Java method with the signature of * the MDX function it implements. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Signature { public abstract String value(); } /** * Annotation which allows you to tag a Java method with the syntax of the * MDX function it implements. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SyntaxDef { public abstract Syntax value(); } /** * Base class for adapter calcs that convert arguments into the precise * type needed. */ private static abstract class AbstractCalc2 extends AbstractCalc { /** * Creates an AbstractCalc2. * * @param exp Source expression * @param calc Child compiled expression */ protected AbstractCalc2(Exp exp, Calc calc) { super(exp, new Calc[] {calc}); } } /** * Calc which calls a Java method. */ private static class JavaMethodCalc extends GenericCalc { private final Method method; private final Object[] args; /** * Creates a JavaMethodCalc. * * @param call Function call being implemented * @param calcs Calcs for arguments of function call * @param method Method to call */ public JavaMethodCalc( ResolvedFunCall call, Calc[] calcs, Method method) { super(call, calcs); this.method = method; this.args = new Object[calcs.length]; } public Object evaluate(Evaluator evaluator) { final Calc[] calcs = getCalcs(); for (int i = 0; i < args.length; i++) { args[i] = calcs[i].evaluate(evaluator); if (args[i] == null) { return nullValue; } } try { return method.invoke(null, args); } catch (IllegalAccessException e) { throw newEvalException(e); } catch (InvocationTargetException e) { throw newEvalException(e.getCause()); } catch (IllegalArgumentException e) { if (e.getMessage().equals("argument type mismatch")) { StringBuilder buf = new StringBuilder( "argument type mismatch: parameters ("); int k = 0; for (Class<?> parameterType : method.getParameterTypes()) { if (k++ > 0) { buf.append(", "); } buf.append(parameterType.getName()); } buf.append("), actual ("); k = 0; for (Object arg : args) { if (k++ > 0) { buf.append(", "); } buf.append( arg == null ? "null" : arg.getClass().getName()); } buf.append(")"); throw newInternal(buf.toString()); } else { throw e; } } } } } // End JavaFunDef.java