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