source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ExpressionFactory.java@ 7138

Last change on this file since 7138 was 7136, checked in by bastiK, 10 years ago

mapcss: add support for alpha info in color property, e.g.
color: #aa0022ee; or color: rgba(1.0, 0.2, 0.8, 0.8);
*opacity properties still have higher priority, if specified
explicitly

File size: 30.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import java.awt.Color;
5import java.io.UnsupportedEncodingException;
6import java.lang.annotation.ElementType;
7import java.lang.annotation.Retention;
8import java.lang.annotation.RetentionPolicy;
9import java.lang.annotation.Target;
10import java.lang.reflect.Array;
11import java.lang.reflect.InvocationTargetException;
12import java.lang.reflect.Method;
13import java.net.URLEncoder;
14import java.nio.charset.StandardCharsets;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.List;
18import java.util.regex.Matcher;
19import java.util.regex.Pattern;
20import java.util.zip.CRC32;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.actions.search.SearchCompiler;
24import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
25import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
26import org.openstreetmap.josm.data.osm.OsmPrimitive;
27import org.openstreetmap.josm.gui.mappaint.Cascade;
28import org.openstreetmap.josm.gui.mappaint.Environment;
29import org.openstreetmap.josm.io.XmlWriter;
30import org.openstreetmap.josm.tools.ColorHelper;
31import org.openstreetmap.josm.tools.Utils;
32
33/**
34 * Factory to generate Expressions.
35 *
36 * See {@link #createFunctionExpression}.
37 */
38public final class ExpressionFactory {
39
40 /**
41 * Marks functions which should be executed also when one or more arguments are null.
42 */
43 @Target(ElementType.METHOD)
44 @Retention(RetentionPolicy.RUNTIME)
45 static @interface NullableArguments {}
46
47 private static final List<Method> arrayFunctions = new ArrayList<>();
48 private static final List<Method> parameterFunctions = new ArrayList<>();
49 private static final List<Method> parameterFunctionsEnv = new ArrayList<>();
50
51 static {
52 for (Method m : Functions.class.getDeclaredMethods()) {
53 Class<?>[] paramTypes = m.getParameterTypes();
54 if (paramTypes.length == 1 && paramTypes[0].isArray()) {
55 arrayFunctions.add(m);
56 } else if (paramTypes.length >= 1 && paramTypes[0].equals(Environment.class)) {
57 parameterFunctionsEnv.add(m);
58 } else {
59 parameterFunctions.add(m);
60 }
61 }
62 try {
63 parameterFunctions.add(Math.class.getMethod("abs", float.class));
64 parameterFunctions.add(Math.class.getMethod("acos", double.class));
65 parameterFunctions.add(Math.class.getMethod("asin", double.class));
66 parameterFunctions.add(Math.class.getMethod("atan", double.class));
67 parameterFunctions.add(Math.class.getMethod("atan2", double.class, double.class));
68 parameterFunctions.add(Math.class.getMethod("ceil", double.class));
69 parameterFunctions.add(Math.class.getMethod("cos", double.class));
70 parameterFunctions.add(Math.class.getMethod("cosh", double.class));
71 parameterFunctions.add(Math.class.getMethod("exp", double.class));
72 parameterFunctions.add(Math.class.getMethod("floor", double.class));
73 parameterFunctions.add(Math.class.getMethod("log", double.class));
74 parameterFunctions.add(Math.class.getMethod("max", float.class, float.class));
75 parameterFunctions.add(Math.class.getMethod("min", float.class, float.class));
76 parameterFunctions.add(Math.class.getMethod("random"));
77 parameterFunctions.add(Math.class.getMethod("round", float.class));
78 parameterFunctions.add(Math.class.getMethod("signum", double.class));
79 parameterFunctions.add(Math.class.getMethod("sin", double.class));
80 parameterFunctions.add(Math.class.getMethod("sinh", double.class));
81 parameterFunctions.add(Math.class.getMethod("sqrt", double.class));
82 parameterFunctions.add(Math.class.getMethod("tan", double.class));
83 parameterFunctions.add(Math.class.getMethod("tanh", double.class));
84 } catch (NoSuchMethodException | SecurityException ex) {
85 throw new RuntimeException(ex);
86 }
87 }
88
89 private ExpressionFactory() {
90 // Hide default constructor for utils classes
91 }
92
93 /**
94 * List of functions that can be used in MapCSS expressions.
95 *
96 * First parameter can be of type {@link Environment} (if needed). This is
97 * automatically filled in by JOSM and the user only sees the remaining
98 * arguments.
99 * When one of the user supplied arguments cannot be converted the
100 * expected type or is null, the function is not called and it returns null
101 * immediately. Add the annotation {@link NullableArguments} to allow
102 * null arguments.
103 * Every method must be static.
104 */
105 @SuppressWarnings("UnusedDeclaration")
106 public static class Functions {
107
108 /**
109 * Identity function for compatibility with MapCSS specification.
110 * @param o any object
111 * @return {@code o} unchanged
112 */
113 public static Object eval(Object o) {
114 return o;
115 }
116
117 public static float plus(float... args) {
118 float res = 0;
119 for (float f : args) {
120 res += f;
121 }
122 return res;
123 }
124
125 public static Float minus(float... args) {
126 if (args.length == 0) {
127 return 0.0F;
128 }
129 if (args.length == 1) {
130 return -args[0];
131 }
132 float res = args[0];
133 for (int i = 1; i < args.length; ++i) {
134 res -= args[i];
135 }
136 return res;
137 }
138
139 public static float times(float... args) {
140 float res = 1;
141 for (float f : args) {
142 res *= f;
143 }
144 return res;
145 }
146
147 public static Float divided_by(float... args) {
148 if (args.length == 0) {
149 return 1.0F;
150 }
151 float res = args[0];
152 for (int i = 1; i < args.length; ++i) {
153 if (args[i] == 0.0F) {
154 return null;
155 }
156 res /= args[i];
157 }
158 return res;
159 }
160
161 /**
162 * Creates a list of values, e.g., for the {@code dashes} property.
163 * @see Arrays#asList(Object[])
164 */
165 public static List<Object> list(Object... args) {
166 return Arrays.asList(args);
167 }
168
169 /**
170 * Returns the first non-null object. The name originates from the {@code COALESCE} SQL function.
171 * @see Utils#firstNonNull(Object[])
172 */
173 @NullableArguments
174 public static Object coalesce(Object... args) {
175 return Utils.firstNonNull(args);
176 }
177
178 /**
179 * Get the {@code n}th element of the list {@code lst} (counting starts at 0).
180 * @since 5699
181 */
182 public static Object get(List<?> lst, float n) {
183 int idx = Math.round(n);
184 if (idx >= 0 && idx < lst.size()) {
185 return lst.get(idx);
186 }
187 return null;
188 }
189
190 /**
191 * Splits string {@code toSplit} at occurrences of the separator string {@code sep} and returns a list of matches.
192 * @see String#split(String)
193 * @since 5699
194 */
195 public static List<String> split(String sep, String toSplit) {
196 return Arrays.asList(toSplit.split(Pattern.quote(sep), -1));
197 }
198
199 /**
200 * 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)
201 * @see Color#Color(float, float, float)
202 */
203 public static Color rgb(float r, float g, float b) {
204 try {
205 return new Color(r, g, b);
206 } catch (IllegalArgumentException e) {
207 return null;
208 }
209 }
210
211 public static Color rgba(float r, float g, float b, float alpha) {
212 try {
213 return new Color(r, g, b, alpha);
214 } catch (IllegalArgumentException e) {
215 return null;
216 }
217 }
218
219 /**
220 * Create color from hsb color model. (arguments form 0.0 to 1.0)
221 * @param h hue
222 * @param s saturation
223 * @param b brightness
224 * @return the corresponding color
225 */
226 public static Color hsb_color(float h, float s, float b) {
227 try {
228 return Color.getHSBColor(h, s, b);
229 } catch (IllegalArgumentException e) {
230 return null;
231 }
232 }
233
234 /**
235 * Creates a color value from an HTML notation, i.e., {@code #rrggbb}.
236 */
237 public static Color html2color(String html) {
238 return ColorHelper.html2color(html);
239 }
240
241 /**
242 * Computes the HTML notation ({@code #rrggbb}) for a color value).
243 */
244 public static String color2html(Color c) {
245 return ColorHelper.color2html(c);
246 }
247
248 /**
249 * Get the value of the red color channel in the rgb color model
250 * @return the red color channel in the range [0;1]
251 * @see java.awt.Color#getRed()
252 */
253 public static float red(Color c) {
254 return Utils.color_int2float(c.getRed());
255 }
256
257 /**
258 * Get the value of the green color channel in the rgb color model
259 * @return the green color channel in the range [0;1]
260 * @see java.awt.Color#getGreen()
261 */
262 public static float green(Color c) {
263 return Utils.color_int2float(c.getGreen());
264 }
265
266 /**
267 * Get the value of the blue color channel in the rgb color model
268 * @return the blue color channel in the range [0;1]
269 * @see java.awt.Color#getBlue()
270 */
271 public static float blue(Color c) {
272 return Utils.color_int2float(c.getBlue());
273 }
274
275 /**
276 * Get the value of the alpha channel in the rgba color model
277 * @return the alpha channel in the range [0;1]
278 * @see java.awt.Color#getAlpha()
279 */
280 public static float alpha(Color c) {
281 return Utils.color_int2float(c.getAlpha());
282 }
283
284 /**
285 * Assembles the strings to one.
286 * @see Utils#join
287 */
288 @NullableArguments
289 public static String concat(Object... args) {
290 return Utils.join("", Arrays.asList(args));
291 }
292
293 /**
294 * Assembles the strings to one, where the first entry is used as separator.
295 * @see Utils#join
296 */
297 @NullableArguments
298 public static String join(String... args) {
299 return Utils.join(args[0], Arrays.asList(args).subList(1, args.length));
300 }
301
302 /**
303 * Returns the value of the property {@code key}, e.g., {@code prop("width")}.
304 */
305 public static Object prop(final Environment env, String key) {
306 return prop(env, key, null);
307 }
308
309 /**
310 * Returns the value of the property {@code key} from layer {@code layer}.
311 */
312 public static Object prop(final Environment env, String key, String layer) {
313 return env.getCascade(layer).get(key);
314 }
315
316 /**
317 * Determines whether property {@code key} is set.
318 */
319 public static Boolean is_prop_set(final Environment env, String key) {
320 return is_prop_set(env, key, null);
321 }
322
323 /**
324 * Determines whether property {@code key} is set on layer {@code layer}.
325 */
326 public static Boolean is_prop_set(final Environment env, String key, String layer) {
327 return env.getCascade(layer).containsKey(key);
328 }
329
330 /**
331 * Gets the value of the key {@code key} from the object in question.
332 */
333 public static String tag(final Environment env, String key) {
334 return env.osm == null ? null : env.osm.get(key);
335 }
336
337 /**
338 * Gets the first non-null value of the key {@code key} from the object's parent(s).
339 */
340 public static String parent_tag(final Environment env, String key) {
341 if (env.parent == null) {
342 if (env.osm != null) {
343 // we don't have a matched parent, so just search all referrers
344 for (OsmPrimitive parent : env.osm.getReferrers()) {
345 String value = parent.get(key);
346 if (value != null) {
347 return value;
348 }
349 }
350 }
351 return null;
352 }
353 return env.parent.get(key);
354 }
355
356 public static String child_tag(final Environment env, String key) {
357 return env.child == null ? null : env.child.get(key);
358 }
359
360 /**
361 * Determines whether the object has a tag with the given key.
362 */
363 public static boolean has_tag_key(final Environment env, String key) {
364 return env.osm.hasKey(key);
365 }
366
367 /**
368 * Returns the index of node in parent way or member in parent relation.
369 */
370 public static Float index(final Environment env) {
371 if (env.index == null) {
372 return null;
373 }
374 return new Float(env.index + 1);
375 }
376
377 public static String role(final Environment env) {
378 return env.getRole();
379 }
380
381 public static boolean not(boolean b) {
382 return !b;
383 }
384
385 public static boolean greater_equal(float a, float b) {
386 return a >= b;
387 }
388
389 public static boolean less_equal(float a, float b) {
390 return a <= b;
391 }
392
393 public static boolean greater(float a, float b) {
394 return a > b;
395 }
396
397 public static boolean less(float a, float b) {
398 return a < b;
399 }
400
401 /**
402 * Determines if the objects {@code a} and {@code b} are equal.
403 * @see Object#equals(Object)
404 */
405 public static boolean equal(Object a, Object b) {
406 // make sure the casts are done in a meaningful way, so
407 // the 2 objects really can be considered equal
408 for (Class<?> klass : new Class<?>[]{Float.class, Boolean.class, Color.class, float[].class, String.class}) {
409 Object a2 = Cascade.convertTo(a, klass);
410 Object b2 = Cascade.convertTo(b, klass);
411 if (a2 != null && b2 != null && a2.equals(b2)) {
412 return true;
413 }
414 }
415 return false;
416 }
417
418 /**
419 * Determines whether the JOSM search with {@code searchStr} applies to the object.
420 */
421 public static Boolean JOSM_search(final Environment env, String searchStr) {
422 Match m;
423 try {
424 m = SearchCompiler.compile(searchStr, false, false);
425 } catch (ParseError ex) {
426 return null;
427 }
428 return m.match(env.osm);
429 }
430
431 /**
432 * Obtains the JOSM'key {@link org.openstreetmap.josm.data.Preferences} string for key {@code key},
433 * and defaults to {@code def} if that is null.
434 * @see org.openstreetmap.josm.data.Preferences#get(String, String)
435 */
436 public static String JOSM_pref(String key, String def) {
437 String res = Main.pref.get(key, null);
438 return res != null ? res : def;
439 }
440
441 /**
442 * Obtains the JOSM'key {@link org.openstreetmap.josm.data.Preferences} color for key {@code key},
443 * and defaults to {@code def} if that is null.
444 * @see org.openstreetmap.josm.data.Preferences#getColor(String, java.awt.Color)
445 */
446 public static Color JOSM_pref_color(String key, Color def) {
447 Color res = Main.pref.getColor(key, null);
448 return res != null ? res : def;
449 }
450
451 /**
452 * Tests if string {@code target} matches pattern {@code pattern}
453 * @see Pattern#matches(String, CharSequence)
454 * @since 5699
455 */
456 public static boolean regexp_test(String pattern, String target) {
457 return Pattern.matches(pattern, target);
458 }
459
460 /**
461 * Tests if string {@code target} matches pattern {@code pattern}
462 * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
463 * @since 5699
464 */
465 public static boolean regexp_test(String pattern, String target, String flags) {
466 int f = 0;
467 if (flags.contains("i")) {
468 f |= Pattern.CASE_INSENSITIVE;
469 }
470 if (flags.contains("s")) {
471 f |= Pattern.DOTALL;
472 }
473 if (flags.contains("m")) {
474 f |= Pattern.MULTILINE;
475 }
476 return Pattern.compile(pattern, f).matcher(target).matches();
477 }
478
479 /**
480 * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
481 * The first element (index 0) is the complete match (i.e. string).
482 * Further elements correspond to the bracketed parts of the regular expression.
483 * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
484 * @since 5701
485 */
486 public static List<String> regexp_match(String pattern, String target, String flags) {
487 int f = 0;
488 if (flags.contains("i")) {
489 f |= Pattern.CASE_INSENSITIVE;
490 }
491 if (flags.contains("s")) {
492 f |= Pattern.DOTALL;
493 }
494 if (flags.contains("m")) {
495 f |= Pattern.MULTILINE;
496 }
497 Matcher m = Pattern.compile(pattern, f).matcher(target);
498 return Utils.getMatches(m);
499 }
500
501 /**
502 * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
503 * The first element (index 0) is the complete match (i.e. string).
504 * Further elements correspond to the bracketed parts of the regular expression.
505 * @since 5701
506 */
507 public static List<String> regexp_match(String pattern, String target) {
508 Matcher m = Pattern.compile(pattern).matcher(target);
509 return Utils.getMatches(m);
510 }
511
512 /**
513 * Returns the OSM id of the current object.
514 * @see OsmPrimitive#getUniqueId()
515 */
516 public static long osm_id(final Environment env) {
517 return env.osm.getUniqueId();
518 }
519
520 /**
521 * Translates some text for the current locale. The first argument is the text to translate,
522 * and the subsequent arguments are parameters for the string indicated by {@code {0}}, {@code {1}}, …
523 */
524 @NullableArguments
525 public static String tr(String... args) {
526 final String text = args[0];
527 System.arraycopy(args, 1, args, 0, args.length - 1);
528 return org.openstreetmap.josm.tools.I18n.tr(text, (Object[])args);
529 }
530
531 /**
532 * Returns the substring of {@code s} starting at index {@code begin} (inclusive, 0-indexed).
533 * @param s The base string
534 * @param begin The start index
535 * @return the substring
536 * @see String#substring(int)
537 */
538 public static String substring(String s, /* due to missing Cascade.convertTo for int*/ float begin) {
539 return s == null ? null : s.substring((int) begin);
540 }
541
542 /**
543 * Returns the substring of {@code s} starting at index {@code begin} (inclusive)
544 * and ending at index {@code end}, (exclusive, 0-indexed).
545 * @param s The base string
546 * @param begin The start index
547 * @param end The end index
548 * @return the substring
549 * @see String#substring(int, int)
550 */
551 public static String substring(String s, float begin, float end) {
552 return s == null ? null : s.substring((int) begin, (int) end);
553 }
554
555 /**
556 * Replaces in {@code s} every {@code} target} substring by {@code replacement}.
557 * * @see String#replace(CharSequence, CharSequence)
558 */
559 public static String replace(String s, String target, String replacement) {
560 return s == null ? null : s.replace(target, replacement);
561 }
562
563 /**
564 * Percent-encode a string. (See https://en.wikipedia.org/wiki/Percent-encoding)
565 * This is especially useful for data urls, e.g.
566 * <code>icon-image: concat("data:image/svg+xml,", URL_encode("&lt;svg&gt;...&lt;/svg&gt;"));</code>
567 * @param s arbitrary string
568 * @return the encoded string
569 */
570 public static String URL_encode(String s) {
571 try {
572 return s == null ? null : URLEncoder.encode(s, "UTF-8");
573 } catch (UnsupportedEncodingException ex) {
574 throw new RuntimeException(ex);
575 }
576 }
577
578 /**
579 * XML-encode a string.
580 *
581 * Escapes special characters in xml. Alternative to using &lt;![CDATA[ ... ]]&gt; blocks.
582 * @param s arbitrary string
583 * @return the encoded string
584 */
585 public static String XML_encode(String s) {
586 return s == null ? null : XmlWriter.encode(s);
587 }
588
589 /**
590 * Calculates the CRC32 checksum from a string (based on RFC 1952).
591 * @param s the string
592 * @return long value from 0 to 2^32-1
593 */
594 public static long CRC32_checksum(String s) {
595 CRC32 cs = new CRC32();
596 cs.update(s.getBytes(StandardCharsets.UTF_8));
597 return cs.getValue();
598 }
599 }
600
601 /**
602 * Main method to create an function-like expression.
603 *
604 * @param name the name of the function or operator
605 * @param args the list of arguments (as expressions)
606 * @return the generated Expression. If no suitable function can be found,
607 * returns {@link NullExpression#INSTANCE}.
608 */
609 public static Expression createFunctionExpression(String name, List<Expression> args) {
610 if ("cond".equals(name) && args.size() == 3)
611 return new CondOperator(args.get(0), args.get(1), args.get(2));
612 else if ("and".equals(name))
613 return new AndOperator(args);
614 else if ("or".equals(name))
615 return new OrOperator(args);
616 else if ("length".equals(name) && args.size() == 1)
617 return new LengthFunction(args.get(0));
618
619 for (Method m : arrayFunctions) {
620 if (m.getName().equals(name))
621 return new ArrayFunction(m, args);
622 }
623 for (Method m : parameterFunctions) {
624 if (m.getName().equals(name) && args.size() == m.getParameterTypes().length)
625 return new ParameterFunction(m, args, false);
626 }
627 for (Method m : parameterFunctionsEnv) {
628 if (m.getName().equals(name) && args.size() == m.getParameterTypes().length-1)
629 return new ParameterFunction(m, args, true);
630 }
631 return NullExpression.INSTANCE;
632 }
633
634 /**
635 * Expression that always evaluates to null.
636 */
637 public static class NullExpression implements Expression {
638
639 /**
640 * The unique instance.
641 */
642 public static final NullExpression INSTANCE = new NullExpression();
643
644 @Override
645 public Object evaluate(Environment env) {
646 return null;
647 }
648 }
649
650 /**
651 * Conditional operator.
652 */
653 public static class CondOperator implements Expression {
654
655 private Expression condition, firstOption, secondOption;
656
657 public CondOperator(Expression condition, Expression firstOption, Expression secondOption) {
658 this.condition = condition;
659 this.firstOption = firstOption;
660 this.secondOption = secondOption;
661 }
662
663 @Override
664 public Object evaluate(Environment env) {
665 Boolean b = Cascade.convertTo(condition.evaluate(env), boolean.class);
666 if (b != null && b)
667 return firstOption.evaluate(env);
668 else
669 return secondOption.evaluate(env);
670 }
671 }
672
673 public static class AndOperator implements Expression {
674
675 private List<Expression> args;
676
677 public AndOperator(List<Expression> args) {
678 this.args = args;
679 }
680
681 @Override
682 public Object evaluate(Environment env) {
683 for (Expression arg : args) {
684 Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
685 if (b == null || !b) {
686 return false;
687 }
688 }
689 return true;
690 }
691 }
692
693 public static class OrOperator implements Expression {
694
695 private List<Expression> args;
696
697 public OrOperator(List<Expression> args) {
698 this.args = args;
699 }
700
701 @Override
702 public Object evaluate(Environment env) {
703 for (Expression arg : args) {
704 Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
705 if (b != null && b) {
706 return true;
707 }
708 }
709 return false;
710 }
711 }
712
713 /**
714 * Function to calculate the length of a string or list in a MapCSS eval
715 * expression.
716 *
717 * Separate implementation to support overloading for different
718 * argument types.
719 */
720 public static class LengthFunction implements Expression {
721
722 private Expression arg;
723
724 public LengthFunction(Expression args) {
725 this.arg = args;
726 }
727
728 @Override
729 public Object evaluate(Environment env) {
730 List<?> l = Cascade.convertTo(arg.evaluate(env), List.class);
731 if (l != null)
732 return l.size();
733 String s = Cascade.convertTo(arg.evaluate(env), String.class);
734 if (s != null)
735 return s.length();
736 return null;
737 }
738 }
739
740 /**
741 * Function that takes a certain number of argument with specific type.
742 *
743 * Implementation is based on a Method object.
744 * If any of the arguments evaluate to null, the result will also be null.
745 */
746 public static class ParameterFunction implements Expression {
747
748 private final Method m;
749 private final boolean nullable;
750 private final List<Expression> args;
751 private final Class<?>[] expectedParameterTypes;
752 private final boolean needsEnvironment;
753
754 public ParameterFunction(Method m, List<Expression> args, boolean needsEnvironment) {
755 this.m = m;
756 this.nullable = m.getAnnotation(NullableArguments.class) != null;
757 this.args = args;
758 this.expectedParameterTypes = m.getParameterTypes();
759 this.needsEnvironment = needsEnvironment;
760 }
761
762 @Override
763 public Object evaluate(Environment env) {
764 Object[] convertedArgs;
765
766 if (needsEnvironment) {
767 convertedArgs = new Object[args.size()+1];
768 convertedArgs[0] = env;
769 for (int i = 1; i < convertedArgs.length; ++i) {
770 convertedArgs[i] = Cascade.convertTo(args.get(i-1).evaluate(env), expectedParameterTypes[i]);
771 if (convertedArgs[i] == null && !nullable) {
772 return null;
773 }
774 }
775 } else {
776 convertedArgs = new Object[args.size()];
777 for (int i = 0; i < convertedArgs.length; ++i) {
778 convertedArgs[i] = Cascade.convertTo(args.get(i).evaluate(env), expectedParameterTypes[i]);
779 if (convertedArgs[i] == null && !nullable) {
780 return null;
781 }
782 }
783 }
784 Object result = null;
785 try {
786 result = m.invoke(null, convertedArgs);
787 } catch (IllegalAccessException | IllegalArgumentException ex) {
788 throw new RuntimeException(ex);
789 } catch (InvocationTargetException ex) {
790 Main.error(ex);
791 return null;
792 }
793 return result;
794 }
795
796 @Override
797 public String toString() {
798 StringBuilder b = new StringBuilder("ParameterFunction~");
799 b.append(m.getName()).append("(");
800 for (int i = 0; i < args.size(); ++i) {
801 if (i > 0) b.append(",");
802 b.append(expectedParameterTypes[i]);
803 b.append(" ").append(args.get(i));
804 }
805 b.append(')');
806 return b.toString();
807 }
808
809 }
810
811 /**
812 * Function that takes an arbitrary number of arguments.
813 *
814 * Currently, all array functions are static, so there is no need to
815 * provide the environment, like it is done in {@link ParameterFunction}.
816 * If any of the arguments evaluate to null, the result will also be null.
817 */
818 public static class ArrayFunction implements Expression {
819
820 private final Method m;
821 private final boolean nullable;
822 private final List<Expression> args;
823 private final Class<?>[] expectedParameterTypes;
824 private final Class<?> arrayComponentType;
825
826 public ArrayFunction(Method m, List<Expression> args) {
827 this.m = m;
828 this.nullable = m.getAnnotation(NullableArguments.class) != null;
829 this.args = args;
830 this.expectedParameterTypes = m.getParameterTypes();
831 this.arrayComponentType = expectedParameterTypes[0].getComponentType();
832 }
833
834 @Override
835 public Object evaluate(Environment env) {
836 Object[] convertedArgs = new Object[expectedParameterTypes.length];
837 Object arrayArg = Array.newInstance(arrayComponentType, args.size());
838 for (int i = 0; i < args.size(); ++i) {
839 Object o = Cascade.convertTo(args.get(i).evaluate(env), arrayComponentType);
840 if (o == null && !nullable) {
841 return null;
842 }
843 Array.set(arrayArg, i, o);
844 }
845 convertedArgs[0] = arrayArg;
846
847 Object result = null;
848 try {
849 result = m.invoke(null, convertedArgs);
850 } catch (IllegalAccessException | IllegalArgumentException ex) {
851 throw new RuntimeException(ex);
852 } catch (InvocationTargetException ex) {
853 Main.error(ex);
854 return null;
855 }
856 return result;
857 }
858 @Override
859 public String toString() {
860 StringBuilder b = new StringBuilder("ArrayFunction~");
861 b.append(m.getName()).append("(");
862 for (int i = 0; i < args.size(); ++i) {
863 if (i > 0) b.append(",");
864 b.append(arrayComponentType);
865 b.append(" ").append(args.get(i));
866 }
867 b.append(')');
868 return b.toString();
869 }
870
871 }
872
873}
Note: See TracBrowser for help on using the repository browser.