001/* 002// This software is subject to the terms of the Eclipse Public License v1.0 003// Agreement, available at the following URL: 004// http://www.eclipse.org/legal/epl-v10.html. 005// You must accept the terms of that agreement to use this software. 006// 007// Copyright (C) 2007-2009 Pentaho 008// All Rights Reserved. 009*/ 010package mondrian.olap.fun; 011 012import mondrian.calc.*; 013import mondrian.calc.impl.AbstractCalc; 014import mondrian.calc.impl.GenericCalc; 015import mondrian.mdx.ResolvedFunCall; 016import mondrian.olap.*; 017 018import java.lang.annotation.*; 019import java.lang.reflect.*; 020import java.util.*; 021 022/** 023 * MDX function which is implemented by a Java method. When the function is 024 * executed, the method is invoked via reflection. 025 * 026 * @author wgorman, jhyde 027 * @since Jan 5, 2008 028*/ 029public class JavaFunDef extends FunDefBase { 030 private static final Map<Class, Integer> mapClazzToCategory = 031 new HashMap<Class, Integer>(); 032 private static final String className = JavaFunDef.class.getName(); 033 034 static { 035 mapClazzToCategory.put(String.class, Category.String); 036 mapClazzToCategory.put(Double.class, Category.Numeric); 037 mapClazzToCategory.put(double.class, Category.Numeric); 038 mapClazzToCategory.put(Integer.class, Category.Integer); 039 mapClazzToCategory.put(int.class, Category.Integer); 040 mapClazzToCategory.put(boolean.class, Category.Logical); 041 mapClazzToCategory.put(Object.class, Category.Value); 042 mapClazzToCategory.put(Date.class, Category.DateTime); 043 mapClazzToCategory.put(float.class, Category.Numeric); 044 mapClazzToCategory.put(long.class, Category.Numeric); 045 mapClazzToCategory.put(double[].class, Category.Array); 046 mapClazzToCategory.put(char.class, Category.String); 047 mapClazzToCategory.put(byte.class, Category.Integer); 048 } 049 050 private final Method method; 051 052 /** 053 * Creates a JavaFunDef. 054 * 055 * @param name Name 056 * @param desc Description 057 * @param syntax Syntax 058 * @param returnCategory Return type 059 * @param paramCategories Parameter types 060 * @param method Java method which implements this function 061 */ 062 public JavaFunDef( 063 String name, 064 String desc, 065 Syntax syntax, 066 int returnCategory, 067 int[] paramCategories, 068 Method method) 069 { 070 super(name, null, desc, syntax, returnCategory, paramCategories); 071 this.method = method; 072 } 073 074 public Calc compileCall( 075 ResolvedFunCall call, 076 ExpCompiler compiler) 077 { 078 final Calc[] calcs = new Calc[parameterCategories.length]; 079 final Class<?>[] parameterTypes = method.getParameterTypes(); 080 for (int i = 0; i < calcs.length;i++) { 081 calcs[i] = 082 compileTo( 083 compiler, call.getArgs()[i], parameterTypes[i]); 084 } 085 return new JavaMethodCalc(call, calcs, method); 086 } 087 088 private static int getCategory(Class clazz) { 089 return mapClazzToCategory.get(clazz); 090 } 091 092 private static int getReturnCategory(Method m) { 093 return getCategory(m.getReturnType()); 094 } 095 096 private static int[] getParameterCategories(Method m) { 097 int arr[] = new int[m.getParameterTypes().length]; 098 for (int i = 0; i < m.getParameterTypes().length; i++) { 099 arr[i] = getCategory(m.getParameterTypes()[i]); 100 } 101 return arr; 102 } 103 104 private static FunDef generateFunDef(final Method method) { 105 String name = 106 getAnnotation( 107 method, className + "$FunctionName", method.getName()); 108 String desc = 109 getAnnotation( 110 method, className + "$Description", ""); 111 Syntax syntax = 112 getAnnotation( 113 method, className + "$SyntaxDef", Syntax.Function); 114 115 // In JDK 1.4 we don't have annotations, so the function name will be 116 // precisely the method name. In particular, we went the 117 // Vba.int_(Object) method to become the 'Int' function. 118 if (name.endsWith("_") && Util.PreJdk15) { 119 name = name.substring(0, name.length() - 1); 120 } 121 122 int returnCategory = getReturnCategory(method); 123 124 int paramCategories[] = getParameterCategories(method); 125 126 return new JavaFunDef( 127 name, desc, syntax, returnCategory, paramCategories, method); 128 } 129 130 /** 131 * Scans a java class and returns a list of function definitions, one for 132 * each static method which is suitable to become an MDX function. 133 * 134 * @param clazz Class 135 * @return List of function definitions 136 */ 137 public static List<FunDef> scan(Class clazz) { 138 List<FunDef> list = new ArrayList<FunDef>(); 139 Method[] methods = clazz.getMethods(); 140 for (Method method : methods) { 141 if (Modifier.isStatic(method.getModifiers()) 142 && !method.getName().equals("main")) 143 { 144 list.add(generateFunDef(method)); 145 } 146 } 147 return list; 148 } 149 150 /** 151 * Compiles an expression to a calc of the required result type. 152 * 153 * <p>Since the result of evaluating the calc will be passed to the method 154 * using reflection, it is important that the calc returns 155 * <em>precisely</em> the correct type: if a method requires an 156 * <code>int</code>, you can pass an {@link Integer} but not a {@link Long} 157 * or {@link Float}. 158 * 159 * <p>If it can be determined that the underlying calc will never return 160 * null, generates an optimal form with one fewer object instantiation. 161 * 162 * @param compiler Compiler 163 * @param exp Expression to compile 164 * @param clazz Desired class 165 * @return compiled expression 166 */ 167 private static Calc compileTo(ExpCompiler compiler, Exp exp, Class clazz) { 168 if (clazz == String.class) { 169 return compiler.compileString(exp); 170 } else if (clazz == Date.class) { 171 return compiler.compileDateTime(exp); 172 } else if (clazz == boolean.class) { 173 return compiler.compileBoolean(exp); 174 } else if (clazz == byte.class) { 175 final IntegerCalc integerCalc = compiler.compileInteger(exp); 176 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 177 // We know that the calculation will never return a null value, 178 // so generate optimized code. 179 return new AbstractCalc2(exp, integerCalc) { 180 public Object evaluate(Evaluator evaluator) { 181 return (byte) integerCalc.evaluateInteger(evaluator); 182 } 183 }; 184 } else { 185 return new AbstractCalc2(exp, integerCalc) { 186 public Object evaluate(Evaluator evaluator) { 187 Integer i = (Integer) integerCalc.evaluate(evaluator); 188 return i == null ? null : (byte) i.intValue(); 189 } 190 }; 191 } 192 } else if (clazz == char.class) { 193 final StringCalc stringCalc = compiler.compileString(exp); 194 return new AbstractCalc2(exp, stringCalc) { 195 public Object evaluate(Evaluator evaluator) { 196 final String string = 197 stringCalc.evaluateString(evaluator); 198 return 199 Character.valueOf( 200 string == null 201 || string.length() < 1 202 ? (char) 0 203 : string.charAt(0)); 204 } 205 }; 206 } else if (clazz == short.class) { 207 final IntegerCalc integerCalc = compiler.compileInteger(exp); 208 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 209 return new AbstractCalc2(exp, integerCalc) { 210 public Object evaluate(Evaluator evaluator) { 211 return (short) integerCalc.evaluateInteger(evaluator); 212 } 213 }; 214 } else { 215 return new AbstractCalc2(exp, integerCalc) { 216 public Object evaluate(Evaluator evaluator) { 217 Integer i = (Integer) integerCalc.evaluate(evaluator); 218 return i == null ? null : (short) i.intValue(); 219 } 220 }; 221 } 222 } else if (clazz == int.class) { 223 return compiler.compileInteger(exp); 224 } else if (clazz == long.class) { 225 final IntegerCalc integerCalc = compiler.compileInteger(exp); 226 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 227 return new AbstractCalc2(exp, integerCalc) { 228 public Object evaluate(Evaluator evaluator) { 229 return (long) integerCalc.evaluateInteger(evaluator); 230 } 231 }; 232 } else { 233 return new AbstractCalc2(exp, integerCalc) { 234 public Object evaluate(Evaluator evaluator) { 235 Integer i = (Integer) integerCalc.evaluate(evaluator); 236 return i == null ? null : (long) i.intValue(); 237 } 238 }; 239 } 240 } else if (clazz == float.class) { 241 final DoubleCalc doubleCalc = compiler.compileDouble(exp); 242 if (doubleCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 243 return new AbstractCalc2(exp, doubleCalc) { 244 public Object evaluate(Evaluator evaluator) { 245 Double v = (Double) doubleCalc.evaluate(evaluator); 246 return v == null ? null : v.floatValue(); 247 } 248 }; 249 } else { 250 return new AbstractCalc2(exp, doubleCalc) { 251 public Object evaluate(Evaluator evaluator) { 252 return (float) doubleCalc.evaluateDouble(evaluator); 253 } 254 }; 255 } 256 } else if (clazz == double.class) { 257 return compiler.compileDouble(exp); 258 } else if (clazz == Object.class) { 259 return compiler.compileScalar(exp, false); 260 } else { 261 throw newInternal("expected primitive type, got " + clazz); 262 } 263 } 264 265 /** 266 * Annotation which allows you to tag a Java method with the name of the 267 * MDX function it implements. 268 */ 269 @Retention(RetentionPolicy.RUNTIME) 270 @Target(ElementType.METHOD) 271 public @interface FunctionName 272 { 273 public abstract String value(); 274 } 275 276 /** 277 * Annotation which allows you to tag a Java method with the description 278 * of the MDX function it implements. 279 */ 280 @Retention(RetentionPolicy.RUNTIME) 281 @Target(ElementType.METHOD) 282 public @interface Description 283 { 284 public abstract String value(); 285 } 286 287 /** 288 * Annotation which allows you to tag a Java method with the signature of 289 * the MDX function it implements. 290 */ 291 @Retention(RetentionPolicy.RUNTIME) 292 @Target(ElementType.METHOD) 293 public @interface Signature 294 { 295 public abstract String value(); 296 } 297 298 /** 299 * Annotation which allows you to tag a Java method with the syntax of the 300 * MDX function it implements. 301 */ 302 @Retention(RetentionPolicy.RUNTIME) 303 @Target(ElementType.METHOD) 304 public @interface SyntaxDef 305 { 306 public abstract Syntax value(); 307 } 308 309 /** 310 * Base class for adapter calcs that convert arguments into the precise 311 * type needed. 312 */ 313 private static abstract class AbstractCalc2 extends AbstractCalc { 314 /** 315 * Creates an AbstractCalc2. 316 * 317 * @param exp Source expression 318 * @param calc Child compiled expression 319 */ 320 protected AbstractCalc2(Exp exp, Calc calc) { 321 super(exp, new Calc[] {calc}); 322 } 323 } 324 325 /** 326 * Calc which calls a Java method. 327 */ 328 private static class JavaMethodCalc extends GenericCalc { 329 private final Method method; 330 private final Object[] args; 331 332 /** 333 * Creates a JavaMethodCalc. 334 * 335 * @param call Function call being implemented 336 * @param calcs Calcs for arguments of function call 337 * @param method Method to call 338 */ 339 public JavaMethodCalc( 340 ResolvedFunCall call, Calc[] calcs, Method method) 341 { 342 super(call, calcs); 343 this.method = method; 344 this.args = new Object[calcs.length]; 345 } 346 347 public Object evaluate(Evaluator evaluator) { 348 final Calc[] calcs = getCalcs(); 349 for (int i = 0; i < args.length; i++) { 350 args[i] = calcs[i].evaluate(evaluator); 351 if (args[i] == null) { 352 return nullValue; 353 } 354 } 355 try { 356 return method.invoke(null, args); 357 } catch (IllegalAccessException e) { 358 throw newEvalException(e); 359 } catch (InvocationTargetException e) { 360 throw newEvalException(e.getCause()); 361 } catch (IllegalArgumentException e) { 362 if (e.getMessage().equals("argument type mismatch")) { 363 StringBuilder buf = 364 new StringBuilder( 365 "argument type mismatch: parameters ("); 366 int k = 0; 367 for (Class<?> parameterType : method.getParameterTypes()) { 368 if (k++ > 0) { 369 buf.append(", "); 370 } 371 buf.append(parameterType.getName()); 372 } 373 buf.append("), actual ("); 374 k = 0; 375 for (Object arg : args) { 376 if (k++ > 0) { 377 buf.append(", "); 378 } 379 buf.append( 380 arg == null 381 ? "null" 382 : arg.getClass().getName()); 383 } 384 buf.append(")"); 385 throw newInternal(buf.toString()); 386 } else { 387 throw e; 388 } 389 } 390 } 391 } 392} 393 394// End JavaFunDef.java