001package jmri.jmrit.logixng; 002 003import java.io.*; 004import java.nio.charset.StandardCharsets; 005import java.util.*; 006 007import javax.script.*; 008 009import org.slf4j.Logger; 010 011import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 012import jmri.*; 013import jmri.jmrit.logixng.Stack.ValueAndType; 014import jmri.jmrit.logixng.util.ReferenceUtil; 015import jmri.jmrit.logixng.util.parser.*; 016import jmri.script.JmriScriptEngineManager; 017import jmri.util.TypeConversionUtil; 018 019/** 020 * A symbol table 021 * 022 * @author Daniel Bergqvist 2020 023 */ 024public interface SymbolTable { 025 026 /** 027 * The list of symbols in the table 028 * @return the symbols 029 */ 030 Map<String, Symbol> getSymbols(); 031 032 /** 033 * The list of symbols and their values in the table 034 * @return the name of the symbols and their values 035 */ 036 Map<String, Object> getSymbolValues(); 037 038 /** 039 * Get the value of a symbol 040 * @param name the name 041 * @return the value 042 */ 043 Object getValue(String name); 044 045 /** 046 * Get the value and type of a symbol. 047 * This method does not lookup global variables. 048 * @param name the name 049 * @return the value and type 050 */ 051 ValueAndType getValueAndType(String name); 052 053 /** 054 * Is the symbol in the symbol table? 055 * @param name the name 056 * @return true if the symbol exists, false otherwise 057 */ 058 boolean hasValue(String name); 059 060 /** 061 * Set the value of a symbol 062 * @param name the name 063 * @param value the value 064 */ 065 void setValue(String name, Object value); 066 067 /** 068 * Add new symbols to the symbol table 069 * @param symbolDefinitions the definitions of the new symbols 070 * @throws JmriException if an exception is thrown 071 */ 072 void createSymbols(Collection<? extends VariableData> symbolDefinitions) 073 throws JmriException; 074 075 /** 076 * Add new symbols to the symbol table. 077 * This method is used for parameters, when new symbols might be created 078 * that uses symbols from a previous symbol table. 079 * 080 * @param symbolTable the symbol table to get existing symbols from 081 * @param symbolDefinitions the definitions of the new symbols 082 * @throws JmriException if an exception is thrown 083 */ 084 void createSymbols( 085 SymbolTable symbolTable, 086 Collection<? extends VariableData> symbolDefinitions) 087 throws JmriException; 088 089 /** 090 * Removes symbols from the symbol table 091 * @param symbolDefinitions the definitions of the symbols to be removed 092 * @throws JmriException if an exception is thrown 093 */ 094 void removeSymbols(Collection<? extends VariableData> symbolDefinitions) 095 throws JmriException; 096 097 /** 098 * Print the symbol table on a stream 099 * @param stream the stream 100 */ 101 void printSymbolTable(java.io.PrintWriter stream); 102 103 /** 104 * Validates the name of a symbol 105 * @param name the name 106 * @return true if the name is valid, false otherwise 107 */ 108 static boolean validateName(String name) { 109 if (name.isEmpty()) return false; 110 if (!Character.isLetter(name.charAt(0))) return false; 111 for (int i=0; i < name.length(); i++) { 112 if (!Character.isLetterOrDigit(name.charAt(i)) && (name.charAt(i) != '_')) { 113 return false; 114 } 115 } 116 return true; 117 } 118 119 /** 120 * Get the stack. 121 * This method is only used internally by DefaultSymbolTable. 122 * 123 * @return the stack 124 */ 125 Stack getStack(); 126 127 128 /** 129 * An enum that defines the types of initial value. 130 */ 131 enum InitialValueType { 132 133 None(Bundle.getMessage("InitialValueType_None"), true), 134 Boolean(Bundle.getMessage("InitialValueType_Boolean"), true), 135 Integer(Bundle.getMessage("InitialValueType_Integer"), true), 136 FloatingNumber(Bundle.getMessage("InitialValueType_FloatingNumber"), true), 137 String(Bundle.getMessage("InitialValueType_String"), true), 138 Array(Bundle.getMessage("InitialValueType_Array"), false), 139 Map(Bundle.getMessage("InitialValueType_Map"), false), 140 LocalVariable(Bundle.getMessage("InitialValueType_LocalVariable"), true), 141 Memory(Bundle.getMessage("InitialValueType_Memory"), true), 142 Reference(Bundle.getMessage("InitialValueType_Reference"), true), 143 Formula(Bundle.getMessage("InitialValueType_Formula"), true), 144 ScriptExpression(Bundle.getMessage("InitialValueType_ScriptExpression"), true), 145 ScriptFile(Bundle.getMessage("InitialValueType_ScriptFile"), true), 146 LogixNG_Table(Bundle.getMessage("InitialValueType_LogixNGTable"), true), 147 148 // This can't be selected by the user. It's only used internally. 149 Object(Bundle.getMessage("InitialValueType_None"), false, false); 150 151 152 private final String _descr; 153 private final boolean _isValidAsParameter; 154 private final boolean _isVisible; 155 156 private InitialValueType(String descr, boolean isValidAsParameter) { 157 this(descr, isValidAsParameter, true); 158 } 159 160 private InitialValueType(String descr, boolean isValidAsParameter, boolean isVisible) { 161 _descr = descr; 162 _isValidAsParameter = isValidAsParameter; 163 _isVisible = isVisible; 164 } 165 166 @Override 167 public String toString() { 168 return _descr; 169 } 170 171 public boolean isValidAsParameter() { 172 return _isValidAsParameter; 173 } 174 175 public boolean isVisible() { 176 return _isVisible; 177 } 178 } 179 180 181 /** 182 * The definition of the symbol 183 */ 184 interface Symbol { 185 186 /** 187 * The name of the symbol 188 * @return the name 189 */ 190 String getName(); 191 192 /** 193 * The index on the stack for this symbol 194 * @return the index 195 */ 196 int getIndex(); 197 198 } 199 200 201 /** 202 * Data for a variable. 203 */ 204 static class VariableData { 205 206 public String _name; 207 public InitialValueType _initialValueType = InitialValueType.None; 208 public String _initialValueData; 209 210 public VariableData( 211 String name, 212 InitialValueType initialValueType, 213 String initialValueData) { 214 215 _name = name; 216 if (initialValueType != null) { 217 _initialValueType = initialValueType; 218 } 219 _initialValueData = initialValueData; 220 } 221 222 public VariableData(VariableData variableData) { 223 _name = variableData._name; 224 _initialValueType = variableData._initialValueType; 225 _initialValueData = variableData._initialValueData; 226 } 227 228 /** 229 * The name of the variable 230 * @return the name 231 */ 232 public String getName() { 233 return _name; 234 } 235 236 public InitialValueType getInitialValueType() { 237 return _initialValueType; 238 } 239 240 public String getInitialValueData() { 241 return _initialValueData; 242 } 243 244 } 245 246 /** 247 * Print a variable 248 * @param log the logger 249 * @param pad the padding 250 * @param name the name 251 * @param value the value 252 * @param expandArraysAndMaps true if arrays and maps should be expanded, false otherwise 253 * @param showClassName true if class name should be shown 254 * @param headerName header for the variable name 255 * @param headerValue header for the variable value 256 */ 257 @SuppressWarnings("unchecked") // Checked cast is not possible due to type erasure 258 @SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", justification="The code prints a complex variable, like a map, to the log") 259 static void printVariable( 260 Logger log, 261 String pad, 262 String name, 263 Object value, 264 boolean expandArraysAndMaps, 265 boolean showClassName, 266 String headerName, 267 String headerValue) { 268 269 if (expandArraysAndMaps && (value instanceof Map)) { 270 log.warn("{}{}: {},", pad, headerName, name); 271 var map = ((Map<? extends Object, ? extends Object>)value); 272 for (var entry : map.entrySet()) { 273 String className = showClassName && entry.getValue() != null 274 ? ", " + entry.getValue().getClass().getName() 275 : ""; 276 log.warn("{}{}{} -> {}{},", pad, pad, entry.getKey(), entry.getValue(), className); 277 } 278 } else if (expandArraysAndMaps && (value instanceof List)) { 279 log.warn("{}{}: {},", pad, headerName, name); 280 var list = ((List<? extends Object>)value); 281 for (int i=0; i < list.size(); i++) { 282 Object val = list.get(i); 283 String className = showClassName && val != null 284 ? ", " + val.getClass().getName() 285 : ""; 286 log.warn("{}{}{}: {}{},", pad, pad, i, val, className); 287 } 288 } else { 289 String className = showClassName && value != null 290 ? ", " + value.getClass().getName() 291 : ""; 292 if (value instanceof NamedBean) { 293 // Show display name instead of system name 294 value = ((NamedBean)value).getDisplayName(); 295 } 296 log.warn("{}{}: {}, {}: {}{}", pad, headerName, name, headerValue, value, className); 297 } 298 } 299 300 private static Object runScriptExpression(SymbolTable symbolTable, String initialData) { 301 String script = 302 "import jmri\n" + 303 "variable.set(" + initialData + ")"; 304 305 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 306 307 Bindings bindings = new SimpleBindings(); 308 LogixNG_ScriptBindings.addScriptBindings(bindings); 309 310 var variable = new Reference<Object>(); 311 bindings.put("variable", variable); 312 313 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 314 315 try { 316 String theScript = String.format("import jmri%n") + script; 317 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 318 .eval(theScript, bindings); 319 } catch (ScriptException e) { 320 log.warn("cannot execute script", e); 321 return null; 322 } 323 return variable.get(); 324 } 325 326 private static Object runScriptFile(SymbolTable symbolTable, String initialData) { 327 328 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 329 330 Bindings bindings = new SimpleBindings(); 331 LogixNG_ScriptBindings.addScriptBindings(bindings); 332 333 var variable = new Reference<Object>(); 334 bindings.put("variable", variable); 335 336 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 337 338 try (InputStreamReader reader = new InputStreamReader( 339 new FileInputStream(jmri.util.FileUtil.getExternalFilename(initialData)), 340 StandardCharsets.UTF_8)) { 341 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 342 .eval(reader, bindings); 343 } catch (IOException | ScriptException e) { 344 log.warn("cannot execute script \"{}\"", initialData, e); 345 return null; 346 } 347 return variable.get(); 348 } 349 350 private static Object copyLogixNG_Table(String initialData) { 351 352 NamedTable myTable = InstanceManager.getDefault(NamedTableManager.class) 353 .getNamedTable(initialData); 354 355 var myMap = new java.util.concurrent.ConcurrentHashMap<Object, Map<Object, Object>>(); 356 357 for (int row=1; row <= myTable.numRows(); row++) { 358 Object rowKey = myTable.getCell(row, 0); 359 var rowMap = new java.util.concurrent.ConcurrentHashMap<Object, Object>(); 360 361 for (int col=1; col <= myTable.numColumns(); col++) { 362 var columnKey = myTable.getCell(0, col); 363 var cellValue = myTable.getCell(row, col); 364 rowMap.put(columnKey, cellValue); 365 } 366 367 myMap.put(rowKey, rowMap); 368 } 369 370 return myMap; 371 } 372 373 374 enum Type { 375 Global("global variable"), 376 Local("local variable"), 377 Parameter("parameter"); 378 379 private final String _descr; 380 381 private Type(String descr) { 382 _descr = descr; 383 } 384 } 385 386 387 private static void validateValue(Type type, String name, String initialData, String descr) { 388 if (initialData == null) { 389 throw new IllegalArgumentException(String.format("Initial data is null for %s \"%s\". Can't set value %s.", type._descr, name, descr)); 390 } 391 if (initialData.isBlank()) { 392 throw new IllegalArgumentException(String.format("Initial data is empty string for %s \"%s\". Can't set value %s.", type._descr, name, descr)); 393 } 394 } 395 396 static Object getInitialValue( 397 Type type, 398 String name, 399 InitialValueType initialType, 400 String initialData, 401 SymbolTable symbolTable, 402 Map<String, Symbol> symbols) 403 throws ParserException, JmriException { 404 405 switch (initialType) { 406 case None: 407 return null; 408 409 case Boolean: 410 validateValue(type, name, initialData, "to boolean"); 411 return TypeConversionUtil.convertToBoolean(initialData, true); 412 413 case Integer: 414 validateValue(type, name, initialData, "to integer"); 415 return Long.valueOf(initialData); 416 417 case FloatingNumber: 418 validateValue(type, name, initialData, "to floating number"); 419 return Double.valueOf(initialData); 420 421 case String: 422 return initialData; 423 424 case Array: 425 List<Object> array = new java.util.ArrayList<>(); 426 Object initialValue = array; 427 String initialValueData = initialData; 428 if ((initialValueData != null) && !initialValueData.isEmpty()) { 429 Object data = ""; 430 String[] parts = initialValueData.split(":", 2); 431 if (parts.length > 1) { 432 initialValueData = parts[0]; 433 if (Character.isDigit(parts[1].charAt(0))) { 434 try { 435 data = Long.valueOf(parts[1]); 436 } catch (NumberFormatException e) { 437 try { 438 data = Double.valueOf(parts[1]); 439 } catch (NumberFormatException e2) { 440 throw new IllegalArgumentException("Data is not a number", e2); 441 } 442 } 443 } else if ((parts[1].charAt(0) == '"') && (parts[1].charAt(parts[1].length()-1) == '"')) { 444 data = parts[1].substring(1,parts[1].length()-1); 445 } else { 446 // Assume initial value is a local variable 447 data = symbolTable.getValue(parts[1]).toString(); 448 } 449 } 450 try { 451 int count; 452 if (Character.isDigit(initialValueData.charAt(0))) { 453 count = Integer.parseInt(initialValueData); 454 } else { 455 // Assume size is a local variable 456 count = Integer.parseInt(symbolTable.getValue(initialValueData).toString()); 457 } 458 for (int i=0; i < count; i++) array.add(data); 459 } catch (NumberFormatException e) { 460 throw new IllegalArgumentException("Initial capacity is not an integer", e); 461 } 462 } 463 return initialValue; 464 465 case Map: 466 return new java.util.HashMap<>(); 467 468 case LocalVariable: 469 validateValue(type, name, initialData, "from local variable"); 470 return symbolTable.getValue(initialData); 471 472 case Memory: 473 validateValue(type, name, initialData, "from memory"); 474 Memory m = InstanceManager.getDefault(MemoryManager.class).getNamedBean(initialData); 475 if (m != null) return m.getValue(); 476 else return null; 477 478 case Reference: 479 validateValue(type, name, initialData, "from reference"); 480 if (ReferenceUtil.isReference(initialData)) { 481 return ReferenceUtil.getReference( 482 symbolTable, initialData); 483 } else { 484 log.error("\"{}\" is not a reference", initialData); 485 return null; 486 } 487 488 case Formula: 489 validateValue(type, name, initialData, "from formula"); 490 RecursiveDescentParser parser = createParser(symbols); 491 ExpressionNode expressionNode = parser.parseExpression( 492 initialData); 493 return expressionNode.calculate(symbolTable); 494 495 case ScriptExpression: 496 validateValue(type, name, initialData, "from script expression"); 497 return runScriptExpression(symbolTable, initialData); 498 499 case ScriptFile: 500 validateValue(type, name, initialData, "from script file"); 501 return runScriptFile(symbolTable, initialData); 502 503 case LogixNG_Table: 504 validateValue(type, name, initialData, "from logixng table"); 505 return copyLogixNG_Table(initialData); 506 507 case Object: 508 return initialData; 509 510 default: 511 log.error("definition._initialValueType has invalid value: {}", initialType.name()); 512 throw new IllegalArgumentException("definition._initialValueType has invalid value: " + initialType.name()); 513 } 514 } 515 516 private static RecursiveDescentParser createParser(Map<String, Symbol> symbols) 517 throws ParserException { 518 Map<String, Variable> variables = new HashMap<>(); 519 520 for (SymbolTable.Symbol symbol : Collections.unmodifiableMap(symbols).values()) { 521 variables.put(symbol.getName(), 522 new LocalVariableExpressionVariable(symbol.getName())); 523 } 524 525 return new RecursiveDescentParser(variables); 526 } 527 528 /** 529 * Validates that the value can be assigned to a local or global variable 530 * of the specified type if strict typing is enforced. The caller must check 531 * first if this method should be called or not. 532 * @param type the type 533 * @param oldValue the old value 534 * @param newValue the new value 535 * @return the value to assign. It might be converted if needed. 536 * @throws NumberFormatException when needed 537 */ 538 public static Object validateStrictTyping(InitialValueType type, Object oldValue, Object newValue) 539 throws NumberFormatException { 540 541 switch (type) { 542 case None: 543 return newValue; 544 case Boolean: 545 return TypeConversionUtil.convertToBoolean(newValue, true); 546 case Integer: 547 return TypeConversionUtil.convertToLong(newValue, true, true); 548 case FloatingNumber: 549 return TypeConversionUtil.convertToDouble(newValue, false, true, true); 550 case String: 551 if (newValue == null) { 552 return null; 553 } 554 return newValue.toString(); 555 default: 556 if (oldValue == null) { 557 return newValue; 558 } 559 throw new IllegalArgumentException(String.format("A variable of type %s cannot change its value", type._descr)); 560 } 561 } 562 563 564 static class SymbolNotFound extends IllegalArgumentException { 565 566 public SymbolNotFound(String message) { 567 super(message); 568 } 569 } 570 571 572 @SuppressFBWarnings(value="SLF4J_LOGGER_SHOULD_BE_PRIVATE", justification="Interfaces cannot have private fields") 573 org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SymbolTable.class); 574 575}