001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import java.lang.annotation.ElementType;
005import java.lang.annotation.Retention;
006import java.lang.annotation.RetentionPolicy;
007import java.lang.annotation.Target;
008import java.lang.reflect.Array;
009import java.lang.reflect.InvocationTargetException;
010import java.lang.reflect.Method;
011import java.util.ArrayList;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.List;
015import java.util.Objects;
016import java.util.function.Function;
017
018import org.openstreetmap.josm.gui.mappaint.Cascade;
019import org.openstreetmap.josm.gui.mappaint.Environment;
020import org.openstreetmap.josm.tools.JosmRuntimeException;
021import org.openstreetmap.josm.tools.Logging;
022import org.openstreetmap.josm.tools.SubclassFilteredCollection;
023import org.openstreetmap.josm.tools.Utils;
024
025/**
026 * Factory to generate {@link Expression}s.
027 * <p>
028 * See {@link #createFunctionExpression}.
029 */
030public final class ExpressionFactory {
031
032    /**
033     * Marks functions which should be executed also when one or more arguments are null.
034     */
035    @Target(ElementType.METHOD)
036    @Retention(RetentionPolicy.RUNTIME)
037    @interface NullableArguments {}
038
039    private static final List<Method> arrayFunctions = new ArrayList<>();
040    private static final List<Method> parameterFunctions = new ArrayList<>();
041    private static final List<Method> parameterFunctionsEnv = new ArrayList<>();
042
043    static {
044        for (Method m : Functions.class.getDeclaredMethods()) {
045            Class<?>[] paramTypes = m.getParameterTypes();
046            if (paramTypes.length == 1 && paramTypes[0].isArray()) {
047                arrayFunctions.add(m);
048            } else if (paramTypes.length >= 1 && paramTypes[0].equals(Environment.class)) {
049                parameterFunctionsEnv.add(m);
050            } else {
051                parameterFunctions.add(m);
052            }
053        }
054        try {
055            parameterFunctions.add(Math.class.getMethod("abs", float.class));
056            parameterFunctions.add(Math.class.getMethod("acos", double.class));
057            parameterFunctions.add(Math.class.getMethod("asin", double.class));
058            parameterFunctions.add(Math.class.getMethod("atan", double.class));
059            parameterFunctions.add(Math.class.getMethod("atan2", double.class, double.class));
060            parameterFunctions.add(Math.class.getMethod("ceil", double.class));
061            parameterFunctions.add(Math.class.getMethod("cos", double.class));
062            parameterFunctions.add(Math.class.getMethod("cosh", double.class));
063            parameterFunctions.add(Math.class.getMethod("exp", double.class));
064            parameterFunctions.add(Math.class.getMethod("floor", double.class));
065            parameterFunctions.add(Math.class.getMethod("log", double.class));
066            parameterFunctions.add(Math.class.getMethod("max", float.class, float.class));
067            parameterFunctions.add(Math.class.getMethod("min", float.class, float.class));
068            parameterFunctions.add(Math.class.getMethod("random"));
069            parameterFunctions.add(Math.class.getMethod("round", float.class));
070            parameterFunctions.add(Math.class.getMethod("signum", double.class));
071            parameterFunctions.add(Math.class.getMethod("sin", double.class));
072            parameterFunctions.add(Math.class.getMethod("sinh", double.class));
073            parameterFunctions.add(Math.class.getMethod("sqrt", double.class));
074            parameterFunctions.add(Math.class.getMethod("tan", double.class));
075            parameterFunctions.add(Math.class.getMethod("tanh", double.class));
076        } catch (NoSuchMethodException | SecurityException ex) {
077            throw new JosmRuntimeException(ex);
078        }
079    }
080
081    private ExpressionFactory() {
082        // Hide default constructor for utils classes
083    }
084
085    /**
086     * Main method to create an function-like expression.
087     *
088     * @param name the name of the function or operator
089     * @param args the list of arguments (as expressions)
090     * @return the generated Expression. If no suitable function can be found,
091     * returns {@link NullExpression#INSTANCE}.
092     */
093    public static Expression createFunctionExpression(String name, List<Expression> args) {
094        if ("cond".equals(name) && args.size() == 3)
095            return new CondOperator(args.get(0), args.get(1), args.get(2));
096        else if ("and".equals(name))
097            return new AndOperator(args);
098        else if ("or".equals(name))
099            return new OrOperator(args);
100        else if ("length".equals(name) && args.size() == 1)
101            return new LengthFunction(args.get(0));
102        else if ("max".equals(name) && !args.isEmpty())
103            return new MinMaxFunction(args, true);
104        else if ("min".equals(name) && !args.isEmpty())
105            return new MinMaxFunction(args, false);
106
107        for (Method m : arrayFunctions) {
108            if (m.getName().equals(name))
109                return new ArrayFunction(m, args);
110        }
111        for (Method m : parameterFunctions) {
112            if (m.getName().equals(name) && args.size() == m.getParameterTypes().length)
113                return new ParameterFunction(m, args, false);
114        }
115        for (Method m : parameterFunctionsEnv) {
116            if (m.getName().equals(name) && args.size() == m.getParameterTypes().length-1)
117                return new ParameterFunction(m, args, true);
118        }
119        return NullExpression.INSTANCE;
120    }
121
122    /**
123     * Expression that always evaluates to null.
124     */
125    public static class NullExpression implements Expression {
126
127        /**
128         * The unique instance.
129         */
130        public static final NullExpression INSTANCE = new NullExpression();
131
132        @Override
133        public Object evaluate(Environment env) {
134            return null;
135        }
136    }
137
138    /**
139     * Conditional operator.
140     */
141    public static class CondOperator implements Expression {
142
143        private final Expression condition, firstOption, secondOption;
144
145        /**
146         * Constructs a new {@code CondOperator}.
147         * @param condition condition
148         * @param firstOption first option
149         * @param secondOption second option
150         */
151        public CondOperator(Expression condition, Expression firstOption, Expression secondOption) {
152            this.condition = condition;
153            this.firstOption = firstOption;
154            this.secondOption = secondOption;
155        }
156
157        @Override
158        public Object evaluate(Environment env) {
159            Boolean b = Cascade.convertTo(condition.evaluate(env), boolean.class);
160            if (b != null && b)
161                return firstOption.evaluate(env);
162            else
163                return secondOption.evaluate(env);
164        }
165    }
166
167    /**
168     * "And" logical operator.
169     */
170    public static class AndOperator implements Expression {
171
172        private final List<Expression> args;
173
174        /**
175         * Constructs a new {@code AndOperator}.
176         * @param args arguments
177         */
178        public AndOperator(List<Expression> args) {
179            this.args = args;
180        }
181
182        @Override
183        public Object evaluate(Environment env) {
184            for (Expression arg : args) {
185                Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
186                if (b == null || !b) {
187                    return Boolean.FALSE;
188                }
189            }
190            return Boolean.TRUE;
191        }
192    }
193
194    /**
195     * "Or" logical operator.
196     */
197    public static class OrOperator implements Expression {
198
199        private final List<Expression> args;
200
201        /**
202         * Constructs a new {@code OrOperator}.
203         * @param args arguments
204         */
205        public OrOperator(List<Expression> args) {
206            this.args = args;
207        }
208
209        @Override
210        public Object evaluate(Environment env) {
211            for (Expression arg : args) {
212                Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
213                if (b != null && b) {
214                    return Boolean.TRUE;
215                }
216            }
217            return Boolean.FALSE;
218        }
219    }
220
221    /**
222     * Function to calculate the length of a string or list in a MapCSS eval expression.
223     *
224     * Separate implementation to support overloading for different argument types.
225     *
226     * The use for calculating the length of a list is deprecated, use
227     * {@link Functions#count(java.util.List)} instead (see #10061).
228     */
229    public static class LengthFunction implements Expression {
230
231        private final Expression arg;
232
233        /**
234         * Constructs a new {@code LengthFunction}.
235         * @param args arguments
236         */
237        public LengthFunction(Expression args) {
238            this.arg = args;
239        }
240
241        @Override
242        public Object evaluate(Environment env) {
243            List<?> l = Cascade.convertTo(arg.evaluate(env), List.class);
244            if (l != null)
245                return l.size();
246            String s = Cascade.convertTo(arg.evaluate(env), String.class);
247            if (s != null)
248                return s.length();
249            return null;
250        }
251    }
252
253    /**
254     * Computes the maximum/minimum value an arbitrary number of floats, or a list of floats.
255     */
256    public static class MinMaxFunction implements Expression {
257
258        private final List<Expression> args;
259        private final boolean computeMax;
260
261        /**
262         * Constructs a new {@code MinMaxFunction}.
263         * @param args arguments
264         * @param computeMax if {@code true}, compute max. If {@code false}, compute min
265         */
266        public MinMaxFunction(final List<Expression> args, final boolean computeMax) {
267            this.args = args;
268            this.computeMax = computeMax;
269        }
270
271        /**
272         * Compute the minimum / maximum over the list
273         * @param lst The list
274         * @return The minimum or maximum depending on {@link #computeMax}
275         */
276        public Float aggregateList(List<?> lst) {
277            final List<Float> floats = Utils.transform(lst, (Function<Object, Float>) x -> Cascade.convertTo(x, float.class));
278            final Collection<Float> nonNullList = SubclassFilteredCollection.filter(floats, Objects::nonNull);
279            return nonNullList.isEmpty() ? (Float) Float.NaN : computeMax ? Collections.max(nonNullList) : Collections.min(nonNullList);
280        }
281
282        @Override
283        public Object evaluate(final Environment env) {
284            List<?> l = Cascade.convertTo(args.get(0).evaluate(env), List.class);
285            if (args.size() != 1 || l == null)
286                l = Utils.transform(args, (Function<Expression, Object>) x -> x.evaluate(env));
287            return aggregateList(l);
288        }
289    }
290
291    /**
292     * Function that takes a certain number of argument with specific type.
293     *
294     * Implementation is based on a Method object.
295     * If any of the arguments evaluate to null, the result will also be null.
296     */
297    public static class ParameterFunction implements Expression {
298
299        private final Method m;
300        private final boolean nullable;
301        private final List<Expression> args;
302        private final Class<?>[] expectedParameterTypes;
303        private final boolean needsEnvironment;
304
305        /**
306         * Constructs a new {@code ParameterFunction}.
307         * @param m method
308         * @param args arguments
309         * @param needsEnvironment whether function needs environment
310         */
311        public ParameterFunction(Method m, List<Expression> args, boolean needsEnvironment) {
312            this.m = m;
313            this.nullable = m.getAnnotation(NullableArguments.class) != null;
314            this.args = args;
315            this.expectedParameterTypes = m.getParameterTypes();
316            this.needsEnvironment = needsEnvironment;
317        }
318
319        /**
320         * Returns the method.
321         * @return the method
322         * @since 14484
323         */
324        public final Method getMethod() {
325            return m;
326        }
327
328        /**
329         * Returns the arguments.
330         * @return the arguments
331         * @since 14484
332         */
333        public final List<Expression> getArgs() {
334            return args;
335        }
336
337        @Override
338        public Object evaluate(Environment env) {
339            Object[] convertedArgs;
340
341            int start = 0;
342            int offset = 0;
343            if (needsEnvironment) {
344                start = 1;
345                offset = 1;
346                convertedArgs = new Object[args.size() + 1];
347                convertedArgs[0] = env;
348            } else {
349                convertedArgs = new Object[args.size()];
350            }
351
352            for (int i = start; i < convertedArgs.length; ++i) {
353                if (!expectedParameterTypes[i].isArray()) {
354                    convertedArgs[i] = Cascade.convertTo(args.get(i - offset).evaluate(env), expectedParameterTypes[i]);
355                } else {
356                    Class<?> clazz = expectedParameterTypes[i].getComponentType();
357                    Object[] varargs = (Object[]) Array.newInstance(clazz, args.size() - i + 1);
358                    for (int j = 0; j < args.size() - i + 1; ++j) {
359                        varargs[j] = Cascade.convertTo(args.get(j + i - 1).evaluate(env), clazz);
360                    }
361                    convertedArgs[i] = expectedParameterTypes[i].cast(varargs);
362                    break;
363                }
364                if (convertedArgs[i] == null && !nullable) {
365                    return null;
366                }
367            }
368
369            Object result = null;
370            try {
371                result = m.invoke(null, convertedArgs);
372            } catch (IllegalAccessException | IllegalArgumentException ex) {
373                throw new JosmRuntimeException(ex);
374            } catch (InvocationTargetException ex) {
375                Logging.error(ex);
376                return null;
377            }
378            return result;
379        }
380
381        @Override
382        public String toString() {
383            StringBuilder b = new StringBuilder("ParameterFunction~");
384            b.append(m.getName()).append('(');
385            for (int i = 0; i < expectedParameterTypes.length; ++i) {
386                if (i > 0) b.append(',');
387                b.append(expectedParameterTypes[i]);
388                if (!needsEnvironment) {
389                    b.append(' ').append(args.get(i));
390                } else if (i > 0) {
391                    b.append(' ').append(args.get(i-1));
392                }
393            }
394            b.append(')');
395            return b.toString();
396        }
397    }
398
399    /**
400     * Function that takes an arbitrary number of arguments.
401     *
402     * Currently, all array functions are static, so there is no need to
403     * provide the environment, like it is done in {@link ParameterFunction}.
404     * If any of the arguments evaluate to null, the result will also be null.
405     */
406    public static class ArrayFunction implements Expression {
407
408        private final Method m;
409        private final boolean nullable;
410        private final List<Expression> args;
411        private final Class<?>[] expectedParameterTypes;
412        private final Class<?> arrayComponentType;
413
414        /**
415         * Constructs a new {@code ArrayFunction}.
416         * @param m method
417         * @param args arguments
418         */
419        public ArrayFunction(Method m, List<Expression> args) {
420            this.m = m;
421            this.nullable = m.getAnnotation(NullableArguments.class) != null;
422            this.args = args;
423            this.expectedParameterTypes = m.getParameterTypes();
424            this.arrayComponentType = expectedParameterTypes[0].getComponentType();
425        }
426
427        @Override
428        public Object evaluate(Environment env) {
429            Object[] convertedArgs = new Object[expectedParameterTypes.length];
430            Object arrayArg = Array.newInstance(arrayComponentType, args.size());
431            for (int i = 0; i < args.size(); ++i) {
432                Object o = Cascade.convertTo(args.get(i).evaluate(env), arrayComponentType);
433                if (o == null && !nullable) {
434                    return null;
435                }
436                Array.set(arrayArg, i, o);
437            }
438            convertedArgs[0] = arrayArg;
439
440            Object result = null;
441            try {
442                result = m.invoke(null, convertedArgs);
443            } catch (IllegalAccessException | IllegalArgumentException ex) {
444                throw new JosmRuntimeException(ex);
445            } catch (InvocationTargetException ex) {
446                Logging.error(ex);
447                return null;
448            }
449            return result;
450        }
451
452        @Override
453        public String toString() {
454            StringBuilder b = new StringBuilder("ArrayFunction~");
455            b.append(m.getName()).append('(');
456            for (int i = 0; i < args.size(); ++i) {
457                if (i > 0) b.append(',');
458                b.append(arrayComponentType).append(' ').append(args.get(i));
459            }
460            b.append(')');
461            return b.toString();
462        }
463    }
464}