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