001 /* 002 // This software is subject to the terms of the Common Public License 003 // Agreement, available at the following URL: 004 // http://www.opensource.org/licenses/cpl.html. 005 // Copyright (C) 2007-2007 Julian Hyde 006 // All Rights Reserved. 007 // You must accept the terms of that agreement to use this software. 008 */ 009 package mondrian.olap.fun; 010 011 import mondrian.olap.*; 012 import mondrian.calc.*; 013 import mondrian.calc.impl.GenericCalc; 014 import mondrian.calc.impl.AbstractCalc; 015 import mondrian.mdx.ResolvedFunCall; 016 017 import java.lang.reflect.*; 018 import java.lang.annotation.*; 019 import java.util.*; 020 021 /** 022 * MDX function which is implemented by a Java method. When the function is 023 * executed, the method is invoked via reflection. 024 * 025 * @author wgorman, jhyde 026 * @version $Id: //open/mondrian/src/main/mondrian/olap/fun/JavaFunDef.java#1 $ 027 * @since Jan 5, 2008 028 */ 029 public 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.Integer); 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 int returnCategory = getReturnCategory(method); 116 117 int paramCategories[] = getParameterCategories(method); 118 119 return new JavaFunDef( 120 name, desc, syntax, returnCategory, paramCategories, method); 121 } 122 123 /** 124 * Scans a java class and returns a list of function definitions, one for 125 * each static method which is suitable to become an MDX function. 126 * 127 * @param clazz Class 128 * @return List of function definitions 129 */ 130 public static List<FunDef> scan(Class clazz) { 131 List<FunDef> list = new ArrayList<FunDef>(); 132 Method[] methods = clazz.getMethods(); 133 for (Method method : methods) { 134 if (Modifier.isStatic(method.getModifiers()) && 135 !method.getName().equals("main")) 136 { 137 list.add(generateFunDef(method)); 138 } 139 } 140 return list; 141 } 142 143 /** 144 * Compiles an expression to a calc of the required result type. 145 * 146 * <p>Since the result of evaluating the calc will be passed to the method 147 * using reflection, it is important that the calc returns 148 * <em>precisely</em> the correct type: if a method requires an 149 * <code>int</code>, you can pass an {@link Integer} but not a {@link Long} 150 * or {@link Float}. 151 * 152 * <p>If it can be determined that the underlying calc will never return 153 * null, generates an optimal form with one fewer object instantiation. 154 * 155 * @param compiler Compiler 156 * @param exp Expression to compile 157 * @param clazz Desired class 158 * @return compiled expression 159 */ 160 private static Calc compileTo(ExpCompiler compiler, Exp exp, Class clazz) { 161 if (clazz == String.class) { 162 return compiler.compileString(exp); 163 } else if (clazz == Date.class) { 164 return compiler.compileDateTime(exp); 165 } else if (clazz == boolean.class) { 166 return compiler.compileBoolean(exp); 167 } else if (clazz == byte.class) { 168 final IntegerCalc integerCalc = compiler.compileInteger(exp); 169 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 170 // We know that the calculation will never return a null value, 171 // so generate optimized code. 172 return new AbstractCalc2(exp, integerCalc) { 173 public Object evaluate(Evaluator evaluator) { 174 return (byte) integerCalc.evaluateInteger(evaluator); 175 } 176 }; 177 } else { 178 return new AbstractCalc2(exp, integerCalc) { 179 public Object evaluate(Evaluator evaluator) { 180 Integer i = (Integer) integerCalc.evaluate(evaluator); 181 return i == null ? null : (byte) i.intValue(); 182 } 183 }; 184 } 185 } else if (clazz == char.class) { 186 final IntegerCalc integerCalc = compiler.compileInteger(exp); 187 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 188 return new AbstractCalc2(exp, integerCalc) { 189 public Object evaluate(Evaluator evaluator) { 190 return (char) integerCalc.evaluateInteger(evaluator); 191 } 192 }; 193 } else { 194 return new AbstractCalc2(exp, integerCalc) { 195 public Object evaluate(Evaluator evaluator) { 196 Integer i = (Integer) integerCalc.evaluate(evaluator); 197 return i == null ? null : (char) i.intValue(); 198 } 199 }; 200 } 201 } else if (clazz == short.class) { 202 final IntegerCalc integerCalc = compiler.compileInteger(exp); 203 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 204 return new AbstractCalc2(exp, integerCalc) { 205 public Object evaluate(Evaluator evaluator) { 206 return (short) integerCalc.evaluateInteger(evaluator); 207 } 208 }; 209 } else { 210 return new AbstractCalc2(exp, integerCalc) { 211 public Object evaluate(Evaluator evaluator) { 212 Integer i = (Integer) integerCalc.evaluate(evaluator); 213 return i == null ? null : (short) i.intValue(); 214 } 215 }; 216 } 217 } else if (clazz == int.class) { 218 return compiler.compileInteger(exp); 219 } else if (clazz == long.class) { 220 final IntegerCalc integerCalc = compiler.compileInteger(exp); 221 if (integerCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 222 return new AbstractCalc2(exp, integerCalc) { 223 public Object evaluate(Evaluator evaluator) { 224 return (long) integerCalc.evaluateInteger(evaluator); 225 } 226 }; 227 } else { 228 return new AbstractCalc2(exp, integerCalc) { 229 public Object evaluate(Evaluator evaluator) { 230 Integer i = (Integer) integerCalc.evaluate(evaluator); 231 return i == null ? null : (long) i.intValue(); 232 } 233 }; 234 } 235 } else if (clazz == float.class) { 236 final DoubleCalc doubleCalc = compiler.compileDouble(exp); 237 if (doubleCalc.getResultStyle() == ResultStyle.VALUE_NOT_NULL) { 238 return new AbstractCalc2(exp, doubleCalc) { 239 public Object evaluate(Evaluator evaluator) { 240 Double v = (Double) doubleCalc.evaluate(evaluator); 241 return v == null ? null : v.floatValue(); 242 } 243 }; 244 } else { 245 return new AbstractCalc2(exp, doubleCalc) { 246 public Object evaluate(Evaluator evaluator) { 247 return (float) doubleCalc.evaluateDouble(evaluator); 248 } 249 }; 250 } 251 } else if (clazz == double.class) { 252 return compiler.compileDouble(exp); 253 } else { 254 throw newInternal("expected primitive type, got " + clazz); 255 } 256 } 257 258 /** 259 * Annotation which allows you to tag a Java method with the name of the 260 * MDX function it implements. 261 */ 262 @Retention(RetentionPolicy.RUNTIME) 263 @Target(ElementType.METHOD) 264 public @interface FunctionName 265 { 266 public abstract String value(); 267 } 268 269 /** 270 * Annotation which allows you to tag a Java method with the description 271 * of the MDX function it implements. 272 */ 273 @Retention(RetentionPolicy.RUNTIME) 274 @Target(ElementType.METHOD) 275 public @interface Description 276 { 277 public abstract String value(); 278 } 279 280 /** 281 * Annotation which allows you to tag a Java method with the signature of 282 * the MDX function it implements. 283 */ 284 @Retention(RetentionPolicy.RUNTIME) 285 @Target(ElementType.METHOD) 286 public @interface Signature 287 { 288 public abstract String value(); 289 } 290 291 /** 292 * Annotation which allows you to tag a Java method with the syntax of the 293 * MDX function it implements. 294 */ 295 @Retention(RetentionPolicy.RUNTIME) 296 @Target(ElementType.METHOD) 297 public @interface SyntaxDef 298 { 299 public abstract Syntax value(); 300 } 301 302 /** 303 * Base class for adapter calcs that convert arguments into the precise 304 * type needed. 305 */ 306 private static abstract class AbstractCalc2 extends AbstractCalc { 307 private final Calc[] calcs; 308 309 protected AbstractCalc2(Exp exp, Calc calc) { 310 super(exp); 311 this.calcs = new Calc[] {calc}; 312 } 313 314 public Calc[] getCalcs() { 315 return calcs; 316 } 317 } 318 319 /** 320 * Calc which calls a Java method. 321 */ 322 private static class JavaMethodCalc extends GenericCalc { 323 private final Calc[] calcs; 324 private final Method method; 325 private final Object[] args; 326 327 /** 328 * Creates a JavaMethodCalc. 329 * 330 * @param call Function call being implemented 331 * @param calcs Calcs for arguments of function call 332 * @param method Method to call 333 */ 334 public JavaMethodCalc( 335 ResolvedFunCall call, Calc[] calcs, Method method) 336 { 337 super(call); 338 this.calcs = calcs; 339 this.method = method; 340 this.args = new Object[calcs.length]; 341 } 342 343 public Calc[] getCalcs() { 344 return calcs; 345 } 346 347 public Object evaluate(Evaluator evaluator) { 348 for (int i = 0; i < args.length; i++) { 349 args[i] = calcs[i].evaluate(evaluator); 350 if (args[i] == null) { 351 return nullValue; 352 } 353 } 354 try { 355 return method.invoke(null, args); 356 } catch (IllegalAccessException e) { 357 throw newEvalException(e); 358 } catch (InvocationTargetException e) { 359 throw newEvalException(e.getCause()); 360 } catch (IllegalArgumentException e) { 361 if (e.getMessage().equals("argument type mismatch")) { 362 StringBuilder buf = 363 new StringBuilder( 364 "argument type mismatch: parameters ("); 365 int k = 0; 366 for (Class<?> parameterType : method.getParameterTypes()) { 367 if (k++ > 0) { 368 buf.append(", "); 369 } 370 buf.append(parameterType.getName()); 371 } 372 buf.append("), actual ("); 373 k = 0; 374 for (Object arg : args) { 375 if (k++ > 0) { 376 buf.append(", "); 377 } 378 buf.append( 379 arg == null 380 ? "null" 381 : arg.getClass().getName()); 382 } 383 buf.append(")"); 384 throw newInternal(buf.toString()); 385 } else { 386 throw e; 387 } 388 } 389 } 390 } 391 } 392 393 // End JavaFunDef.java