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 private final String _descr; 159 private final boolean _isValidAsParameter; 160 161 private InitialValueType(String descr, boolean isValidAsParameter) { 162 _descr = descr; 163 _isValidAsParameter = isValidAsParameter; 164 } 165 166 @Override 167 public String toString() { 168 return _descr; 169 } 170 171 public boolean isValidAsParameter() { 172 return _isValidAsParameter; 173 } 174 } 175 176 177 /** 178 * The definition of the symbol 179 */ 180 interface Symbol { 181 182 /** 183 * The name of the symbol 184 * @return the name 185 */ 186 String getName(); 187 188 /** 189 * The index on the stack for this symbol 190 * @return the index 191 */ 192 int getIndex(); 193 194 } 195 196 197 /** 198 * Data for a variable. 199 */ 200 static class VariableData { 201 202 public String _name; 203 public InitialValueType _initialValueType = InitialValueType.None; 204 public String _initialValueData; 205 206 public VariableData( 207 String name, 208 InitialValueType initialValueType, 209 String initialValueData) { 210 211 _name = name; 212 if (initialValueType != null) { 213 _initialValueType = initialValueType; 214 } 215 _initialValueData = initialValueData; 216 } 217 218 public VariableData(VariableData variableData) { 219 _name = variableData._name; 220 _initialValueType = variableData._initialValueType; 221 _initialValueData = variableData._initialValueData; 222 } 223 224 /** 225 * The name of the variable 226 * @return the name 227 */ 228 public String getName() { 229 return _name; 230 } 231 232 public InitialValueType getInitialValueType() { 233 return _initialValueType; 234 } 235 236 public String getInitialValueData() { 237 return _initialValueData; 238 } 239 240 } 241 242 /** 243 * Print a variable 244 * @param log the logger 245 * @param pad the padding 246 * @param name the name 247 * @param value the value 248 * @param expandArraysAndMaps true if arrays and maps should be expanded, false otherwise 249 * @param showClassName true if class name should be shown 250 * @param headerName header for the variable name 251 * @param headerValue header for the variable value 252 */ 253 @SuppressWarnings("unchecked") // Checked cast is not possible due to type erasure 254 @SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", justification="The code prints a complex variable, like a map, to the log") 255 static void printVariable( 256 Logger log, 257 String pad, 258 String name, 259 Object value, 260 boolean expandArraysAndMaps, 261 boolean showClassName, 262 String headerName, 263 String headerValue) { 264 265 if (expandArraysAndMaps && (value instanceof Map)) { 266 log.warn("{}{}: {},", pad, headerName, name); 267 var map = ((Map<? extends Object, ? extends Object>)value); 268 for (var entry : map.entrySet()) { 269 String className = showClassName && entry.getValue() != null 270 ? ", " + entry.getValue().getClass().getName() 271 : ""; 272 log.warn("{}{}{} -> {}{},", pad, pad, entry.getKey(), entry.getValue(), className); 273 } 274 } else if (expandArraysAndMaps && (value instanceof List)) { 275 log.warn("{}{}: {},", pad, headerName, name); 276 var list = ((List<? extends Object>)value); 277 for (int i=0; i < list.size(); i++) { 278 Object val = list.get(i); 279 String className = showClassName && val != null 280 ? ", " + val.getClass().getName() 281 : ""; 282 log.warn("{}{}{}: {}{},", pad, pad, i, val, className); 283 } 284 } else { 285 String className = showClassName && value != null 286 ? ", " + value.getClass().getName() 287 : ""; 288 if (value instanceof NamedBean) { 289 // Show display name instead of system name 290 value = ((NamedBean)value).getDisplayName(); 291 } 292 log.warn("{}{}: {}, {}: {}{}", pad, headerName, name, headerValue, value, className); 293 } 294 } 295 296 private static Object runScriptExpression(SymbolTable symbolTable, String initialData) { 297 String script = 298 "import jmri\n" + 299 "variable.set(" + initialData + ")"; 300 301 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 302 303 Bindings bindings = new SimpleBindings(); 304 LogixNG_ScriptBindings.addScriptBindings(bindings); 305 306 var variable = new Reference<Object>(); 307 bindings.put("variable", variable); 308 309 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 310 311 try { 312 String theScript = String.format("import jmri%n") + script; 313 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 314 .eval(theScript, bindings); 315 } catch (ScriptException e) { 316 log.warn("cannot execute script", e); 317 return null; 318 } 319 return variable.get(); 320 } 321 322 private static Object runScriptFile(SymbolTable symbolTable, String initialData) { 323 324 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 325 326 Bindings bindings = new SimpleBindings(); 327 LogixNG_ScriptBindings.addScriptBindings(bindings); 328 329 var variable = new Reference<Object>(); 330 bindings.put("variable", variable); 331 332 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 333 334 try (InputStreamReader reader = new InputStreamReader( 335 new FileInputStream(jmri.util.FileUtil.getExternalFilename(initialData)), 336 StandardCharsets.UTF_8)) { 337 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 338 .eval(reader, bindings); 339 } catch (IOException | ScriptException e) { 340 log.warn("cannot execute script \"{}\"", initialData, e); 341 return null; 342 } 343 return variable.get(); 344 } 345 346 private static Object copyLogixNG_Table(String initialData) { 347 348 NamedTable myTable = InstanceManager.getDefault(NamedTableManager.class) 349 .getNamedTable(initialData); 350 351 var myMap = new java.util.concurrent.ConcurrentHashMap<Object, Map<Object, Object>>(); 352 353 for (int row=1; row <= myTable.numRows(); row++) { 354 Object rowKey = myTable.getCell(row, 0); 355 var rowMap = new java.util.concurrent.ConcurrentHashMap<Object, Object>(); 356 357 for (int col=1; col <= myTable.numColumns(); col++) { 358 var columnKey = myTable.getCell(0, col); 359 var cellValue = myTable.getCell(row, col); 360 rowMap.put(columnKey, cellValue); 361 } 362 363 myMap.put(rowKey, rowMap); 364 } 365 366 return myMap; 367 } 368 369 370 enum Type { 371 Global("global variable"), 372 Local("local variable"), 373 Parameter("parameter"); 374 375 private final String _descr; 376 377 private Type(String descr) { 378 _descr = descr; 379 } 380 } 381 382 383 private static void validateValue(Type type, String name, String initialData, String descr) { 384 if (initialData == null) { 385 throw new IllegalArgumentException(String.format("Initial data is null for %s \"%s\". Can't set value %s.", type._descr, name, descr)); 386 } 387 if (initialData.isBlank()) { 388 throw new IllegalArgumentException(String.format("Initial data is empty string for %s \"%s\". Can't set value %s.", type._descr, name, descr)); 389 } 390 } 391 392 static Object getInitialValue( 393 Type type, 394 String name, 395 InitialValueType initialType, 396 String initialData, 397 SymbolTable symbolTable, 398 Map<String, Symbol> symbols) 399 throws ParserException, JmriException { 400 401 switch (initialType) { 402 case None: 403 return null; 404 405 case Boolean: 406 validateValue(type, name, initialData, "to boolean"); 407 return TypeConversionUtil.convertToBoolean(initialData, true); 408 409 case Integer: 410 validateValue(type, name, initialData, "to integer"); 411 return Long.valueOf(initialData); 412 413 case FloatingNumber: 414 validateValue(type, name, initialData, "to floating number"); 415 return Double.valueOf(initialData); 416 417 case String: 418 return initialData; 419 420 case Array: 421 List<Object> array = new java.util.ArrayList<>(); 422 Object initialValue = array; 423 String initialValueData = initialData; 424 if ((initialValueData != null) && !initialValueData.isEmpty()) { 425 Object data = ""; 426 String[] parts = initialValueData.split(":", 2); 427 if (parts.length > 1) { 428 initialValueData = parts[0]; 429 if (Character.isDigit(parts[1].charAt(0))) { 430 try { 431 data = Long.valueOf(parts[1]); 432 } catch (NumberFormatException e) { 433 try { 434 data = Double.valueOf(parts[1]); 435 } catch (NumberFormatException e2) { 436 throw new IllegalArgumentException("Data is not a number", e2); 437 } 438 } 439 } else if ((parts[1].charAt(0) == '"') && (parts[1].charAt(parts[1].length()-1) == '"')) { 440 data = parts[1].substring(1,parts[1].length()-1); 441 } else { 442 // Assume initial value is a local variable 443 data = symbolTable.getValue(parts[1]).toString(); 444 } 445 } 446 try { 447 int count; 448 if (Character.isDigit(initialValueData.charAt(0))) { 449 count = Integer.parseInt(initialValueData); 450 } else { 451 // Assume size is a local variable 452 count = Integer.parseInt(symbolTable.getValue(initialValueData).toString()); 453 } 454 for (int i=0; i < count; i++) array.add(data); 455 } catch (NumberFormatException e) { 456 throw new IllegalArgumentException("Initial capacity is not an integer", e); 457 } 458 } 459 return initialValue; 460 461 case Map: 462 return new java.util.HashMap<>(); 463 464 case LocalVariable: 465 validateValue(type, name, initialData, "from local variable"); 466 return symbolTable.getValue(initialData); 467 468 case Memory: 469 validateValue(type, name, initialData, "from memory"); 470 Memory m = InstanceManager.getDefault(MemoryManager.class).getNamedBean(initialData); 471 if (m != null) return m.getValue(); 472 else return null; 473 474 case Reference: 475 validateValue(type, name, initialData, "from reference"); 476 if (ReferenceUtil.isReference(initialData)) { 477 return ReferenceUtil.getReference( 478 symbolTable, initialData); 479 } else { 480 log.error("\"{}\" is not a reference", initialData); 481 return null; 482 } 483 484 case Formula: 485 validateValue(type, name, initialData, "from formula"); 486 RecursiveDescentParser parser = createParser(symbols); 487 ExpressionNode expressionNode = parser.parseExpression( 488 initialData); 489 return expressionNode.calculate(symbolTable); 490 491 case ScriptExpression: 492 validateValue(type, name, initialData, "from script expression"); 493 return runScriptExpression(symbolTable, initialData); 494 495 case ScriptFile: 496 validateValue(type, name, initialData, "from script file"); 497 return runScriptFile(symbolTable, initialData); 498 499 case LogixNG_Table: 500 validateValue(type, name, initialData, "from logixng table"); 501 return copyLogixNG_Table(initialData); 502 503 default: 504 log.error("definition._initialValueType has invalid value: {}", initialType.name()); 505 throw new IllegalArgumentException("definition._initialValueType has invalid value: " + initialType.name()); 506 } 507 } 508 509 private static RecursiveDescentParser createParser(Map<String, Symbol> symbols) 510 throws ParserException { 511 Map<String, Variable> variables = new HashMap<>(); 512 513 for (SymbolTable.Symbol symbol : Collections.unmodifiableMap(symbols).values()) { 514 variables.put(symbol.getName(), 515 new LocalVariableExpressionVariable(symbol.getName())); 516 } 517 518 return new RecursiveDescentParser(variables); 519 } 520 521 /** 522 * Validates that the value can be assigned to a local or global variable 523 * of the specified type if strict typing is enforced. The caller must check 524 * first if this method should be called or not. 525 * @param type the type 526 * @param oldValue the old value 527 * @param newValue the new value 528 * @return the value to assign. It might be converted if needed. 529 */ 530 public static Object validateStrictTyping(InitialValueType type, Object oldValue, Object newValue) 531 throws NumberFormatException { 532 533 switch (type) { 534 case None: 535 return newValue; 536 case Boolean: 537 return TypeConversionUtil.convertToBoolean(newValue, true); 538 case Integer: 539 return TypeConversionUtil.convertToLong(newValue, true, true); 540 case FloatingNumber: 541 return TypeConversionUtil.convertToDouble(newValue, false, true, true); 542 case String: 543 if (newValue == null) { 544 return null; 545 } 546 return newValue.toString(); 547 default: 548 if (oldValue == null) { 549 return newValue; 550 } 551 throw new IllegalArgumentException(String.format("A variable of type %s cannot change its value", type._descr)); 552 } 553 } 554 555 556 static class SymbolNotFound extends IllegalArgumentException { 557 558 public SymbolNotFound(String message) { 559 super(message); 560 } 561 } 562 563 564 @SuppressFBWarnings(value="SLF4J_LOGGER_SHOULD_BE_PRIVATE", justification="Interfaces cannot have private fields") 565 org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SymbolTable.class); 566 567}