001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import java.awt.Color;
005import java.io.UnsupportedEncodingException;
006import java.lang.annotation.ElementType;
007import java.lang.annotation.Retention;
008import java.lang.annotation.RetentionPolicy;
009import java.lang.annotation.Target;
010import java.lang.reflect.Array;
011import java.lang.reflect.InvocationTargetException;
012import java.lang.reflect.Method;
013import java.net.URLEncoder;
014import java.nio.charset.StandardCharsets;
015import java.util.ArrayList;
016import java.util.Arrays;
017import java.util.Collection;
018import java.util.Collections;
019import java.util.List;
020import java.util.Objects;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023import java.util.zip.CRC32;
024
025import org.openstreetmap.josm.Main;
026import org.openstreetmap.josm.actions.search.SearchCompiler;
027import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
028import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
029import org.openstreetmap.josm.data.osm.Node;
030import org.openstreetmap.josm.data.osm.OsmPrimitive;
031import org.openstreetmap.josm.gui.mappaint.Cascade;
032import org.openstreetmap.josm.gui.mappaint.Environment;
033import org.openstreetmap.josm.io.XmlWriter;
034import org.openstreetmap.josm.tools.ColorHelper;
035import org.openstreetmap.josm.tools.Predicates;
036import org.openstreetmap.josm.tools.RightAndLefthandTraffic;
037import org.openstreetmap.josm.tools.Utils;
038
039/**
040 * Factory to generate Expressions.
041 *
042 * See {@link #createFunctionExpression}.
043 */
044public final class ExpressionFactory {
045
046    /**
047     * Marks functions which should be executed also when one or more arguments are null.
048     */
049    @Target(ElementType.METHOD)
050    @Retention(RetentionPolicy.RUNTIME)
051    static @interface NullableArguments {}
052
053    private static final List<Method> arrayFunctions = new ArrayList<>();
054    private static final List<Method> parameterFunctions = new ArrayList<>();
055    private static final List<Method> parameterFunctionsEnv = new ArrayList<>();
056
057    static {
058        for (Method m : Functions.class.getDeclaredMethods()) {
059            Class<?>[] paramTypes = m.getParameterTypes();
060            if (paramTypes.length == 1 && paramTypes[0].isArray()) {
061                arrayFunctions.add(m);
062            } else if (paramTypes.length >= 1 && paramTypes[0].equals(Environment.class)) {
063                parameterFunctionsEnv.add(m);
064            } else {
065                parameterFunctions.add(m);
066            }
067        }
068        try {
069            parameterFunctions.add(Math.class.getMethod("abs", float.class));
070            parameterFunctions.add(Math.class.getMethod("acos", double.class));
071            parameterFunctions.add(Math.class.getMethod("asin", double.class));
072            parameterFunctions.add(Math.class.getMethod("atan", double.class));
073            parameterFunctions.add(Math.class.getMethod("atan2", double.class, double.class));
074            parameterFunctions.add(Math.class.getMethod("ceil", double.class));
075            parameterFunctions.add(Math.class.getMethod("cos", double.class));
076            parameterFunctions.add(Math.class.getMethod("cosh", double.class));
077            parameterFunctions.add(Math.class.getMethod("exp", double.class));
078            parameterFunctions.add(Math.class.getMethod("floor", double.class));
079            parameterFunctions.add(Math.class.getMethod("log", double.class));
080            parameterFunctions.add(Math.class.getMethod("max", float.class, float.class));
081            parameterFunctions.add(Math.class.getMethod("min", float.class, float.class));
082            parameterFunctions.add(Math.class.getMethod("random"));
083            parameterFunctions.add(Math.class.getMethod("round", float.class));
084            parameterFunctions.add(Math.class.getMethod("signum", double.class));
085            parameterFunctions.add(Math.class.getMethod("sin", double.class));
086            parameterFunctions.add(Math.class.getMethod("sinh", double.class));
087            parameterFunctions.add(Math.class.getMethod("sqrt", double.class));
088            parameterFunctions.add(Math.class.getMethod("tan", double.class));
089            parameterFunctions.add(Math.class.getMethod("tanh", double.class));
090        } catch (NoSuchMethodException | SecurityException ex) {
091            throw new RuntimeException(ex);
092        }
093    }
094
095    private ExpressionFactory() {
096        // Hide default constructor for utils classes
097    }
098
099    /**
100     * List of functions that can be used in MapCSS expressions.
101     *
102     * First parameter can be of type {@link Environment} (if needed). This is
103     * automatically filled in by JOSM and the user only sees the remaining
104     * arguments.
105     * When one of the user supplied arguments cannot be converted the
106     * expected type or is null, the function is not called and it returns null
107     * immediately. Add the annotation {@link NullableArguments} to allow
108     * null arguments.
109     * Every method must be static.
110     */
111    @SuppressWarnings("UnusedDeclaration")
112    public static class Functions {
113
114        /**
115         * Identity function for compatibility with MapCSS specification.
116         * @param o any object
117         * @return {@code o} unchanged
118         */
119        public static Object eval(Object o) {
120            return o;
121        }
122
123        public static float plus(float... args) {
124            float res = 0;
125            for (float f : args) {
126                res += f;
127            }
128            return res;
129        }
130
131        public static Float minus(float... args) {
132            if (args.length == 0) {
133                return 0.0F;
134            }
135            if (args.length == 1) {
136                return -args[0];
137            }
138            float res = args[0];
139            for (int i = 1; i < args.length; ++i) {
140                res -= args[i];
141            }
142            return res;
143        }
144
145        public static float times(float... args) {
146            float res = 1;
147            for (float f : args) {
148                res *= f;
149            }
150            return res;
151        }
152
153        public static Float divided_by(float... args) {
154            if (args.length == 0) {
155                return 1.0F;
156            }
157            float res = args[0];
158            for (int i = 1; i < args.length; ++i) {
159                if (args[i] == 0.0F) {
160                    return null;
161                }
162                res /= args[i];
163            }
164            return res;
165        }
166
167        /**
168         * Creates a list of values, e.g., for the {@code dashes} property.
169         * @see Arrays#asList(Object[])
170         */
171        public static List<Object> list(Object... args) {
172            return Arrays.asList(args);
173        }
174        
175        /**
176         * Returns the number of elements in a list.
177         * @param lst the list
178         * @return length of the list
179         */
180        public static Integer count(List<?> lst) {
181            return lst.size();
182        }
183
184        /**
185         * Returns the first non-null object. The name originates from the {@code COALESCE} SQL function.
186         * @deprecated Deprecated in favour of {@link #any(Object...)} from the MapCSS standard.
187         */
188        @NullableArguments
189        @Deprecated
190        public static Object coalesce(Object... args) {
191            return any(args);
192        }
193
194        /**
195         * Returns the first non-null object.
196         * The name originates from <a href="http://wiki.openstreetmap.org/wiki/MapCSS/0.2/eval">MapCSS standard</a>.
197         * @see #coalesce(Object...)
198         * @see Utils#firstNonNull(Object[])
199         */
200        @NullableArguments
201        public static Object any(Object... args) {
202            return Utils.firstNonNull(args);
203        }
204
205        /**
206         * Get the {@code n}th element of the list {@code lst} (counting starts at 0).
207         * @since 5699
208         */
209        public static Object get(List<?> lst, float n) {
210            int idx = Math.round(n);
211            if (idx >= 0 && idx < lst.size()) {
212                return lst.get(idx);
213            }
214            return null;
215        }
216
217        /**
218         * Splits string {@code toSplit} at occurrences of the separator string {@code sep} and returns a list of matches.
219         * @see String#split(String)
220         * @since 5699
221         */
222        public static List<String> split(String sep, String toSplit) {
223            return Arrays.asList(toSplit.split(Pattern.quote(sep), -1));
224        }
225
226        /**
227         * Creates a color value with the specified amounts of {@code r}ed, {@code g}reen, {@code b}lue (arguments from 0.0 to 1.0)
228         * @see Color#Color(float, float, float)
229         */
230        public static Color rgb(float r, float g, float b) {
231            try {
232                return new Color(r, g, b);
233            } catch (IllegalArgumentException e) {
234                return null;
235            }
236        }
237        
238        public static Color rgba(float r, float g, float b, float alpha) {
239            try {
240                return new Color(r, g, b, alpha);
241            } catch (IllegalArgumentException e) {
242                return null;
243            }
244        }
245
246        /**
247         * Create color from hsb color model. (arguments form 0.0 to 1.0)
248         * @param h hue
249         * @param s saturation
250         * @param b brightness
251         * @return the corresponding color
252         */
253        public static Color hsb_color(float h, float s, float b) {
254            try {
255                return Color.getHSBColor(h, s, b);
256            } catch (IllegalArgumentException e) {
257                return null;
258            }
259        }
260
261        /**
262         * Creates a color value from an HTML notation, i.e., {@code #rrggbb}.
263         */
264        public static Color html2color(String html) {
265            return ColorHelper.html2color(html);
266        }
267
268        /**
269         * Computes the HTML notation ({@code #rrggbb}) for a color value).
270         */
271        public static String color2html(Color c) {
272            return ColorHelper.color2html(c);
273        }
274
275        /**
276         * Get the value of the red color channel in the rgb color model
277         * @return the red color channel in the range [0;1]
278         * @see java.awt.Color#getRed()
279         */
280        public static float red(Color c) {
281            return Utils.color_int2float(c.getRed());
282        }
283
284        /**
285         * Get the value of the green color channel in the rgb color model
286         * @return the green color channel in the range [0;1]
287         * @see java.awt.Color#getGreen()
288         */
289        public static float green(Color c) {
290            return Utils.color_int2float(c.getGreen());
291        }
292
293        /**
294         * Get the value of the blue color channel in the rgb color model
295         * @return the blue color channel in the range [0;1]
296         * @see java.awt.Color#getBlue()
297         */
298        public static float blue(Color c) {
299            return Utils.color_int2float(c.getBlue());
300        }
301
302        /**
303         * Get the value of the alpha channel in the rgba color model
304         * @return the alpha channel in the range [0;1]
305         * @see java.awt.Color#getAlpha()
306         */
307        public static float alpha(Color c) {
308            return Utils.color_int2float(c.getAlpha());
309        }
310
311        /**
312         * Assembles the strings to one.
313         * @see Utils#join
314         */
315        @NullableArguments
316        public static String concat(Object... args) {
317            return Utils.join("", Arrays.asList(args));
318        }
319
320        /**
321         * Assembles the strings to one, where the first entry is used as separator.
322         * @see Utils#join
323         */
324        @NullableArguments
325        public static String join(String... args) {
326            return Utils.join(args[0], Arrays.asList(args).subList(1, args.length));
327        }
328
329        /**
330         * Returns the value of the property {@code key}, e.g., {@code prop("width")}.
331         */
332        public static Object prop(final Environment env, String key) {
333            return prop(env, key, null);
334        }
335
336        /**
337         * Returns the value of the property {@code key} from layer {@code layer}.
338         */
339        public static Object prop(final Environment env, String key, String layer) {
340            return env.getCascade(layer).get(key);
341        }
342
343        /**
344         * Determines whether property {@code key} is set.
345         */
346        public static Boolean is_prop_set(final Environment env, String key) {
347            return is_prop_set(env, key, null);
348        }
349
350        /**
351         * Determines whether property {@code key} is set on layer {@code layer}.
352         */
353        public static Boolean is_prop_set(final Environment env, String key, String layer) {
354            return env.getCascade(layer).containsKey(key);
355        }
356
357        /**
358         * Gets the value of the key {@code key} from the object in question.
359         */
360        public static String tag(final Environment env, String key) {
361            return env.osm == null ? null : env.osm.get(key);
362        }
363
364        /**
365         * Gets the first non-null value of the key {@code key} from the object's parent(s).
366         */
367        public static String parent_tag(final Environment env, String key) {
368            if (env.parent == null) {
369                if (env.osm != null) {
370                    // we don't have a matched parent, so just search all referrers
371                    for (OsmPrimitive parent : env.osm.getReferrers()) {
372                        String value = parent.get(key);
373                        if (value != null) {
374                            return value;
375                        }
376                    }
377                }
378                return null;
379            }
380            return env.parent.get(key);
381        }
382
383        public static String child_tag(final Environment env, String key) {
384            return env.child == null ? null : env.child.get(key);
385        }
386
387        /**
388         * Determines whether the object has a tag with the given key.
389         */
390        public static boolean has_tag_key(final Environment env, String key) {
391            return env.osm.hasKey(key);
392        }
393
394        /**
395         * Returns the index of node in parent way or member in parent relation.
396         */
397        public static Float index(final Environment env) {
398            if (env.index == null) {
399                return null;
400            }
401            return new Float(env.index + 1);
402        }
403
404        public static String role(final Environment env) {
405            return env.getRole();
406        }
407
408        public static boolean not(boolean b) {
409            return !b;
410        }
411
412        public static boolean greater_equal(float a, float b) {
413            return a >= b;
414        }
415
416        public static boolean less_equal(float a, float b) {
417            return a <= b;
418        }
419
420        public static boolean greater(float a, float b) {
421            return a > b;
422        }
423
424        public static boolean less(float a, float b) {
425            return a < b;
426        }
427
428        /**
429         * Determines if the objects {@code a} and {@code b} are equal.
430         * @see Object#equals(Object)
431         */
432        public static boolean equal(Object a, Object b) {
433            if (a.getClass() == b.getClass()) return a.equals(b);
434            if (a.equals(Cascade.convertTo(b, a.getClass()))) return true;
435            return b.equals(Cascade.convertTo(a, b.getClass()));
436        }
437
438        /**
439         * Determines whether the JOSM search with {@code searchStr} applies to the object.
440         */
441        public static Boolean JOSM_search(final Environment env, String searchStr) {
442            Match m;
443            try {
444                m = SearchCompiler.compile(searchStr, false, false);
445            } catch (ParseError ex) {
446                return null;
447            }
448            return m.match(env.osm);
449        }
450
451        /**
452         * Obtains the JOSM'key {@link org.openstreetmap.josm.data.Preferences} string for key {@code key},
453         * and defaults to {@code def} if that is null.
454         * @see org.openstreetmap.josm.data.Preferences#get(String, String)
455         */
456        public static String JOSM_pref(String key, String def) {
457            String res = Main.pref.get(key, null);
458            return res != null ? res : def;
459        }
460
461        /**
462         * Tests if string {@code target} matches pattern {@code pattern}
463         * @see Pattern#matches(String, CharSequence)
464         * @since 5699
465         */
466        public static boolean regexp_test(String pattern, String target) {
467            return Pattern.matches(pattern, target);
468        }
469
470        /**
471         * Tests if string {@code target} matches pattern {@code pattern}
472         * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
473         * @since 5699
474         */
475        public static boolean regexp_test(String pattern, String target, String flags) {
476            int f = 0;
477            if (flags.contains("i")) {
478                f |= Pattern.CASE_INSENSITIVE;
479            }
480            if (flags.contains("s")) {
481                f |= Pattern.DOTALL;
482            }
483            if (flags.contains("m")) {
484                f |= Pattern.MULTILINE;
485            }
486            return Pattern.compile(pattern, f).matcher(target).matches();
487        }
488
489        /**
490         * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
491         * The first element (index 0) is the complete match (i.e. string).
492         * Further elements correspond to the bracketed parts of the regular expression.
493         * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
494         * @since 5701
495         */
496        public static List<String> regexp_match(String pattern, String target, String flags) {
497            int f = 0;
498            if (flags.contains("i")) {
499                f |= Pattern.CASE_INSENSITIVE;
500            }
501            if (flags.contains("s")) {
502                f |= Pattern.DOTALL;
503            }
504            if (flags.contains("m")) {
505                f |= Pattern.MULTILINE;
506            }
507            Matcher m = Pattern.compile(pattern, f).matcher(target);
508            return Utils.getMatches(m);
509        }
510
511        /**
512         * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
513         * The first element (index 0) is the complete match (i.e. string).
514         * Further elements correspond to the bracketed parts of the regular expression.
515         * @since 5701
516         */
517        public static List<String> regexp_match(String pattern, String target) {
518            Matcher m = Pattern.compile(pattern).matcher(target);
519            return Utils.getMatches(m);
520        }
521
522        /**
523         * Returns the OSM id of the current object.
524         * @see OsmPrimitive#getUniqueId()
525         */
526        public static long osm_id(final Environment env) {
527            return env.osm.getUniqueId();
528        }
529
530        /**
531         * Translates some text for the current locale. The first argument is the text to translate,
532         * and the subsequent arguments are parameters for the string indicated by {@code {0}}, {@code {1}}, …
533         */
534        @NullableArguments
535        public static String tr(String... args) {
536            final String text = args[0];
537            System.arraycopy(args, 1, args, 0, args.length - 1);
538            return org.openstreetmap.josm.tools.I18n.tr(text, (Object[])args);
539        }
540
541        /**
542         * Returns the substring of {@code s} starting at index {@code begin} (inclusive, 0-indexed).
543         * @param s The base string
544         * @param begin The start index
545         * @return the substring
546         * @see String#substring(int)
547         */
548        public static String substring(String s, /* due to missing Cascade.convertTo for int*/ float begin) {
549            return s == null ? null : s.substring((int) begin);
550        }
551
552        /**
553         * Returns the substring of {@code s} starting at index {@code begin} (inclusive)
554         * and ending at index {@code end}, (exclusive, 0-indexed).
555         * @param s The base string
556         * @param begin The start index
557         * @param end The end index
558         * @return the substring
559         * @see String#substring(int, int)
560         */
561        public static String substring(String s, float begin, float end) {
562            return s == null ? null : s.substring((int) begin, (int) end);
563        }
564
565        /**
566         * Replaces in {@code s} every {@code} target} substring by {@code replacement}.
567         * * @see String#replace(CharSequence, CharSequence)
568         */
569        public static String replace(String s, String target, String replacement) {
570            return s == null ? null : s.replace(target, replacement);
571        }
572
573        /**
574         * Percent-encode a string. (See https://en.wikipedia.org/wiki/Percent-encoding)
575         * This is especially useful for data urls, e.g.
576         * <code>icon-image: concat("data:image/svg+xml,", URL_encode("&lt;svg&gt;...&lt;/svg&gt;"));</code>
577         * @param s arbitrary string
578         * @return the encoded string
579         */
580        public static String URL_encode(String s) {
581            try {
582                return s == null ? null : URLEncoder.encode(s, "UTF-8");
583            } catch (UnsupportedEncodingException ex) {
584                throw new RuntimeException(ex);
585            }
586        }
587
588        /**
589         * XML-encode a string.
590         *
591         * Escapes special characters in xml. Alternative to using &lt;![CDATA[ ... ]]&gt; blocks.
592         * @param s arbitrary string
593         * @return the encoded string
594         */
595        public static String XML_encode(String s) {
596            return s == null ? null : XmlWriter.encode(s);
597        }
598
599        /**
600         * Calculates the CRC32 checksum from a string (based on RFC 1952).
601         * @param s the string
602         * @return long value from 0 to 2^32-1
603         */
604        public static long CRC32_checksum(String s) {
605            CRC32 cs = new CRC32();
606            cs.update(s.getBytes(StandardCharsets.UTF_8));
607            return cs.getValue();
608        }
609        
610        /**
611         * check if there is right-hand traffic at the current location
612         * @param env the environment
613         * @return true if there is right-hand traffic
614         * @since 7193
615         */
616        public static boolean is_right_hand_traffic(Environment env) {
617            if (env.osm instanceof Node)
618                return RightAndLefthandTraffic.isRightHandTraffic(((Node) env.osm).getCoor());
619            return RightAndLefthandTraffic.isRightHandTraffic(env.osm.getBBox().getCenter());
620        }
621        
622        /**
623         * Prints the object to the command line (for debugging purpose).
624         * @param o the object
625         * @return the same object, unchanged
626         */
627        @NullableArguments
628        public static Object print(Object o) {
629            System.out.print(o == null ? "none" : o.toString());
630            return o;
631        }
632
633        /**
634         * Prints the object to the command line, with new line at the end
635         * (for debugging purpose).
636         * @param o the object
637         * @return the same object, unchanged
638         */
639        @NullableArguments
640        public static Object println(Object o) {
641            System.out.println(o == null ? "none" : o.toString());
642            return o;
643        }
644        
645        /**
646         * Get the number of tags for the current primitive.
647         * @param env
648         * @return number of tags
649         */
650        public static int number_of_tags(Environment env) {
651            return env.osm.getNumKeys();
652        }
653    }
654
655    /**
656     * Main method to create an function-like expression.
657     *
658     * @param name the name of the function or operator
659     * @param args the list of arguments (as expressions)
660     * @return the generated Expression. If no suitable function can be found,
661     * returns {@link NullExpression#INSTANCE}.
662     */
663    public static Expression createFunctionExpression(String name, List<Expression> args) {
664        if ("cond".equals(name) && args.size() == 3)
665            return new CondOperator(args.get(0), args.get(1), args.get(2));
666        else if ("and".equals(name))
667            return new AndOperator(args);
668        else if ("or".equals(name))
669            return new OrOperator(args);
670        else if ("length".equals(name) && args.size() == 1)
671            return new LengthFunction(args.get(0));
672        else if ("max".equals(name) && !args.isEmpty())
673            return new MinMaxFunction(args, true);
674        else if ("min".equals(name) && !args.isEmpty())
675            return new MinMaxFunction(args, false);
676
677        for (Method m : arrayFunctions) {
678            if (m.getName().equals(name))
679                return new ArrayFunction(m, args);
680        }
681        for (Method m : parameterFunctions) {
682            if (m.getName().equals(name) && args.size() == m.getParameterTypes().length)
683                return new ParameterFunction(m, args, false);
684        }
685        for (Method m : parameterFunctionsEnv) {
686            if (m.getName().equals(name) && args.size() == m.getParameterTypes().length-1)
687                return new ParameterFunction(m, args, true);
688        }
689        return NullExpression.INSTANCE;
690    }
691
692    /**
693     * Expression that always evaluates to null.
694     */
695    public static class NullExpression implements Expression {
696
697        /**
698         * The unique instance.
699         */
700        public static final NullExpression INSTANCE = new NullExpression();
701
702        @Override
703        public Object evaluate(Environment env) {
704            return null;
705        }
706    }
707
708    /**
709     * Conditional operator.
710     */
711    public static class CondOperator implements Expression {
712
713        private Expression condition, firstOption, secondOption;
714
715        public CondOperator(Expression condition, Expression firstOption, Expression secondOption) {
716            this.condition = condition;
717            this.firstOption = firstOption;
718            this.secondOption = secondOption;
719        }
720
721        @Override
722        public Object evaluate(Environment env) {
723            Boolean b = Cascade.convertTo(condition.evaluate(env), boolean.class);
724            if (b != null && b)
725                return firstOption.evaluate(env);
726            else
727                return secondOption.evaluate(env);
728        }
729    }
730
731    public static class AndOperator implements Expression {
732
733        private List<Expression> args;
734
735        public AndOperator(List<Expression> args) {
736            this.args = args;
737        }
738
739        @Override
740        public Object evaluate(Environment env) {
741            for (Expression arg : args) {
742                Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
743                if (b == null || !b) {
744                    return false;
745                }
746            }
747            return true;
748        }
749    }
750
751    public static class OrOperator implements Expression {
752
753        private List<Expression> args;
754
755        public OrOperator(List<Expression> args) {
756            this.args = args;
757        }
758
759        @Override
760        public Object evaluate(Environment env) {
761            for (Expression arg : args) {
762                Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
763                if (b != null && b) {
764                    return true;
765                }
766            }
767            return false;
768        }
769    }
770
771    /**
772     * Function to calculate the length of a string or list in a MapCSS eval
773     * expression.
774     *
775     * Separate implementation to support overloading for different
776     * argument types.
777     * 
778     * The use for calculating the length of a list is deprecated, use
779     * {@link Functions#count(java.util.List)} instead (see #10061).
780     */
781    public static class LengthFunction implements Expression {
782
783        private Expression arg;
784
785        public LengthFunction(Expression args) {
786            this.arg = args;
787        }
788
789        @Override
790        public Object evaluate(Environment env) {
791            List<?> l = Cascade.convertTo(arg.evaluate(env), List.class);
792            if (l != null)
793                return l.size();
794            String s = Cascade.convertTo(arg.evaluate(env), String.class);
795            if (s != null)
796                return s.length();
797            return null;
798        }
799    }
800
801    /**
802     * Computes the maximum/minimum value an arbitrary number of floats, or a list of floats.
803     */
804    public static class MinMaxFunction implements Expression {
805
806        private final List<Expression> args;
807        private final boolean computeMax;
808
809        public MinMaxFunction(final List<Expression> args, final boolean computeMax) {
810            this.args = args;
811            this.computeMax = computeMax;
812        }
813
814        public Float aggregateList(List<?> lst) {
815            final List<Float> floats = Utils.transform(lst, new Utils.Function<Object, Float>() {
816                @Override
817                public Float apply(Object x) {
818                    return Cascade.convertTo(x, float.class);
819                }
820            });
821            final Collection<Float> nonNullList = Utils.filter(floats, Predicates.not(Predicates.isNull()));
822            return computeMax ? Collections.max(nonNullList) : Collections.min(nonNullList);
823        }
824
825        @Override
826        public Object evaluate(final Environment env) {
827            List<?> l = Cascade.convertTo(args.get(0).evaluate(env), List.class);
828            if (args.size() != 1 || l == null)
829                l = Utils.transform(args, new Utils.Function<Expression, Object>() {
830                    @Override
831                    public Object apply(Expression x) {
832                        return x.evaluate(env);
833                    }
834                });
835            return aggregateList(l);
836        }
837    }
838
839    /**
840     * Function that takes a certain number of argument with specific type.
841     *
842     * Implementation is based on a Method object.
843     * If any of the arguments evaluate to null, the result will also be null.
844     */
845    public static class ParameterFunction implements Expression {
846
847        private final Method m;
848        private final boolean nullable;
849        private final List<Expression> args;
850        private final Class<?>[] expectedParameterTypes;
851        private final boolean needsEnvironment;
852
853        public ParameterFunction(Method m, List<Expression> args, boolean needsEnvironment) {
854            this.m = m;
855            this.nullable = m.getAnnotation(NullableArguments.class) != null;
856            this.args = args;
857            this.expectedParameterTypes = m.getParameterTypes();
858            this.needsEnvironment = needsEnvironment;
859        }
860
861        @Override
862        public Object evaluate(Environment env) {
863            Object[] convertedArgs;
864
865            if (needsEnvironment) {
866                convertedArgs = new Object[args.size()+1];
867                convertedArgs[0] = env;
868                for (int i = 1; i < convertedArgs.length; ++i) {
869                    convertedArgs[i] = Cascade.convertTo(args.get(i-1).evaluate(env), expectedParameterTypes[i]);
870                    if (convertedArgs[i] == null && !nullable) {
871                        return null;
872                    }
873                }
874            } else {
875                convertedArgs = new Object[args.size()];
876                for (int i = 0; i < convertedArgs.length; ++i) {
877                    convertedArgs[i] = Cascade.convertTo(args.get(i).evaluate(env), expectedParameterTypes[i]);
878                    if (convertedArgs[i] == null && !nullable) {
879                        return null;
880                    }
881                }
882            }
883            Object result = null;
884            try {
885                result = m.invoke(null, convertedArgs);
886            } catch (IllegalAccessException | IllegalArgumentException ex) {
887                throw new RuntimeException(ex);
888            } catch (InvocationTargetException ex) {
889                Main.error(ex);
890                return null;
891            }
892            return result;
893        }
894
895        @Override
896        public String toString() {
897            StringBuilder b = new StringBuilder("ParameterFunction~");
898            b.append(m.getName()).append("(");
899            for (int i = 0; i < args.size(); ++i) {
900                if (i > 0) b.append(",");
901                b.append(expectedParameterTypes[i]);
902                b.append(" ").append(args.get(i));
903            }
904            b.append(')');
905            return b.toString();
906        }
907
908    }
909
910    /**
911     * Function that takes an arbitrary number of arguments.
912     *
913     * Currently, all array functions are static, so there is no need to
914     * provide the environment, like it is done in {@link ParameterFunction}.
915     * If any of the arguments evaluate to null, the result will also be null.
916     */
917    public static class ArrayFunction implements Expression {
918
919        private final Method m;
920        private final boolean nullable;
921        private final List<Expression> args;
922        private final Class<?>[] expectedParameterTypes;
923        private final Class<?> arrayComponentType;
924
925        public ArrayFunction(Method m, List<Expression> args) {
926            this.m = m;
927            this.nullable = m.getAnnotation(NullableArguments.class) != null;
928            this.args = args;
929            this.expectedParameterTypes = m.getParameterTypes();
930            this.arrayComponentType = expectedParameterTypes[0].getComponentType();
931        }
932
933        @Override
934        public Object evaluate(Environment env) {
935            Object[] convertedArgs = new Object[expectedParameterTypes.length];
936            Object arrayArg = Array.newInstance(arrayComponentType, args.size());
937            for (int i = 0; i < args.size(); ++i) {
938                Object o = Cascade.convertTo(args.get(i).evaluate(env), arrayComponentType);
939                if (o == null && !nullable) {
940                    return null;
941                }
942                Array.set(arrayArg, i, o);
943            }
944            convertedArgs[0] = arrayArg;
945
946            Object result = null;
947            try {
948                result = m.invoke(null, convertedArgs);
949            } catch (IllegalAccessException | IllegalArgumentException ex) {
950                throw new RuntimeException(ex);
951            } catch (InvocationTargetException ex) {
952                Main.error(ex);
953                return null;
954            }
955            return result;
956        }
957        @Override
958        public String toString() {
959            StringBuilder b = new StringBuilder("ArrayFunction~");
960            b.append(m.getName()).append("(");
961            for (int i = 0; i < args.size(); ++i) {
962                if (i > 0) b.append(",");
963                b.append(arrayComponentType);
964                b.append(" ").append(args.get(i));
965            }
966            b.append(')');
967            return b.toString();
968        }
969
970    }
971
972}