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("<svg>...</svg>"));</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 <![CDATA[ ... ]]> 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}