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 log.warn("{}{}: {}, {}: {}{}", pad, headerName, name, headerValue, value, className); 289 } 290 } 291 292 private static Object runScriptExpression(SymbolTable symbolTable, String initialData) { 293 String script = 294 "import jmri\n" + 295 "variable.set(" + initialData + ")"; 296 297 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 298 299 Bindings bindings = new SimpleBindings(); 300 LogixNG_ScriptBindings.addScriptBindings(bindings); 301 302 var variable = new Reference<Object>(); 303 bindings.put("variable", variable); 304 305 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 306 307 try { 308 String theScript = String.format("import jmri%n") + script; 309 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 310 .eval(theScript, bindings); 311 } catch (ScriptException e) { 312 log.warn("cannot execute script", e); 313 return null; 314 } 315 return variable.get(); 316 } 317 318 private static Object runScriptFile(SymbolTable symbolTable, String initialData) { 319 320 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 321 322 Bindings bindings = new SimpleBindings(); 323 LogixNG_ScriptBindings.addScriptBindings(bindings); 324 325 var variable = new Reference<Object>(); 326 bindings.put("variable", variable); 327 328 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 329 330 try (InputStreamReader reader = new InputStreamReader( 331 new FileInputStream(jmri.util.FileUtil.getExternalFilename(initialData)), 332 StandardCharsets.UTF_8)) { 333 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 334 .eval(reader, bindings); 335 } catch (IOException | ScriptException e) { 336 log.warn("cannot execute script \"{}\"", initialData, e); 337 return null; 338 } 339 return variable.get(); 340 } 341 342 private static Object copyLogixNG_Table(String initialData) { 343 344 NamedTable myTable = InstanceManager.getDefault(NamedTableManager.class) 345 .getNamedTable(initialData); 346 347 var myMap = new java.util.concurrent.ConcurrentHashMap<Object, Map<Object, Object>>(); 348 349 for (int row=1; row <= myTable.numRows(); row++) { 350 Object rowKey = myTable.getCell(row, 0); 351 var rowMap = new java.util.concurrent.ConcurrentHashMap<Object, Object>(); 352 353 for (int col=1; col <= myTable.numColumns(); col++) { 354 var columnKey = myTable.getCell(0, col); 355 var cellValue = myTable.getCell(row, col); 356 rowMap.put(columnKey, cellValue); 357 } 358 359 myMap.put(rowKey, rowMap); 360 } 361 362 return myMap; 363 } 364 365 366 enum Type { 367 Global("global variable"), 368 Local("local variable"), 369 Parameter("parameter"); 370 371 private final String _descr; 372 373 private Type(String descr) { 374 _descr = descr; 375 } 376 } 377 378 379 private static void validateValue(Type type, String name, String initialData, String descr) { 380 if (initialData == null) { 381 throw new IllegalArgumentException(String.format("Initial data is null for %s \"%s\". Can't set value %s.", type._descr, name, descr)); 382 } 383 if (initialData.isBlank()) { 384 throw new IllegalArgumentException(String.format("Initial data is empty string for %s \"%s\". Can't set value %s.", type._descr, name, descr)); 385 } 386 } 387 388 static Object getInitialValue( 389 Type type, 390 String name, 391 InitialValueType initialType, 392 String initialData, 393 SymbolTable symbolTable, 394 Map<String, Symbol> symbols) 395 throws ParserException, JmriException { 396 397 switch (initialType) { 398 case None: 399 return null; 400 401 case Boolean: 402 validateValue(type, name, initialData, "to boolean"); 403 return TypeConversionUtil.convertToBoolean(initialData, true); 404 405 case Integer: 406 validateValue(type, name, initialData, "to integer"); 407 return Long.valueOf(initialData); 408 409 case FloatingNumber: 410 validateValue(type, name, initialData, "to floating number"); 411 return Double.valueOf(initialData); 412 413 case String: 414 return initialData; 415 416 case Array: 417 List<Object> array = new java.util.ArrayList<>(); 418 Object initialValue = array; 419 String initialValueData = initialData; 420 if ((initialValueData != null) && !initialValueData.isEmpty()) { 421 Object data = ""; 422 String[] parts = initialValueData.split(":", 2); 423 if (parts.length > 1) { 424 initialValueData = parts[0]; 425 if (Character.isDigit(parts[1].charAt(0))) { 426 try { 427 data = Long.valueOf(parts[1]); 428 } catch (NumberFormatException e) { 429 try { 430 data = Double.valueOf(parts[1]); 431 } catch (NumberFormatException e2) { 432 throw new IllegalArgumentException("Data is not a number", e2); 433 } 434 } 435 } else if ((parts[1].charAt(0) == '"') && (parts[1].charAt(parts[1].length()-1) == '"')) { 436 data = parts[1].substring(1,parts[1].length()-1); 437 } else { 438 // Assume initial value is a local variable 439 data = symbolTable.getValue(parts[1]).toString(); 440 } 441 } 442 try { 443 int count; 444 if (Character.isDigit(initialValueData.charAt(0))) { 445 count = Integer.parseInt(initialValueData); 446 } else { 447 // Assume size is a local variable 448 count = Integer.parseInt(symbolTable.getValue(initialValueData).toString()); 449 } 450 for (int i=0; i < count; i++) array.add(data); 451 } catch (NumberFormatException e) { 452 throw new IllegalArgumentException("Initial capacity is not an integer", e); 453 } 454 } 455 return initialValue; 456 457 case Map: 458 return new java.util.HashMap<>(); 459 460 case LocalVariable: 461 validateValue(type, name, initialData, "from local variable"); 462 return symbolTable.getValue(initialData); 463 464 case Memory: 465 validateValue(type, name, initialData, "from memory"); 466 Memory m = InstanceManager.getDefault(MemoryManager.class).getNamedBean(initialData); 467 if (m != null) return m.getValue(); 468 else return null; 469 470 case Reference: 471 validateValue(type, name, initialData, "from reference"); 472 if (ReferenceUtil.isReference(initialData)) { 473 return ReferenceUtil.getReference( 474 symbolTable, initialData); 475 } else { 476 log.error("\"{}\" is not a reference", initialData); 477 return null; 478 } 479 480 case Formula: 481 validateValue(type, name, initialData, "from formula"); 482 RecursiveDescentParser parser = createParser(symbols); 483 ExpressionNode expressionNode = parser.parseExpression( 484 initialData); 485 return expressionNode.calculate(symbolTable); 486 487 case ScriptExpression: 488 validateValue(type, name, initialData, "from script expression"); 489 return runScriptExpression(symbolTable, initialData); 490 491 case ScriptFile: 492 validateValue(type, name, initialData, "from script file"); 493 return runScriptFile(symbolTable, initialData); 494 495 case LogixNG_Table: 496 validateValue(type, name, initialData, "from logixng table"); 497 return copyLogixNG_Table(initialData); 498 499 default: 500 log.error("definition._initialValueType has invalid value: {}", initialType.name()); 501 throw new IllegalArgumentException("definition._initialValueType has invalid value: " + initialType.name()); 502 } 503 } 504 505 private static RecursiveDescentParser createParser(Map<String, Symbol> symbols) 506 throws ParserException { 507 Map<String, Variable> variables = new HashMap<>(); 508 509 for (SymbolTable.Symbol symbol : Collections.unmodifiableMap(symbols).values()) { 510 variables.put(symbol.getName(), 511 new LocalVariableExpressionVariable(symbol.getName())); 512 } 513 514 return new RecursiveDescentParser(variables); 515 } 516 517 /** 518 * Validates that the value can be assigned to a local or global variable 519 * of the specified type if strict typing is enforced. The caller must check 520 * first if this method should be called or not. 521 * @param type the type 522 * @param oldValue the old value 523 * @param newValue the new value 524 * @return the value to assign. It might be converted if needed. 525 */ 526 public static Object validateStrictTyping(InitialValueType type, Object oldValue, Object newValue) 527 throws NumberFormatException { 528 529 switch (type) { 530 case None: 531 return newValue; 532 case Boolean: 533 return TypeConversionUtil.convertToBoolean(newValue, true); 534 case Integer: 535 return TypeConversionUtil.convertToLong(newValue, true, true); 536 case FloatingNumber: 537 return TypeConversionUtil.convertToDouble(newValue, false, true, true); 538 case String: 539 if (newValue == null) { 540 return null; 541 } 542 return newValue.toString(); 543 default: 544 if (oldValue == null) { 545 return newValue; 546 } 547 throw new IllegalArgumentException(String.format("A variable of type %s cannot change its value", type._descr)); 548 } 549 } 550 551 552 static class SymbolNotFound extends IllegalArgumentException { 553 554 public SymbolNotFound(String message) { 555 super(message); 556 } 557 } 558 559 560 @SuppressFBWarnings(value="SLF4J_LOGGER_SHOULD_BE_PRIVATE", justification="Interfaces cannot have private fields") 561 org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SymbolTable.class); 562 563}