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