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