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