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.util.ReferenceUtil; 020import jmri.jmrit.logixng.util.parser.*; 021import jmri.jmrit.logixng.util.parser.ExpressionNode; 022import jmri.jmrit.logixng.util.parser.LocalVariableExpressionVariable; 023import jmri.script.JmriScriptEngineManager; 024 025import org.slf4j.Logger; 026 027/** 028 * A symbol table 029 * 030 * @author Daniel Bergqvist 2020 031 */ 032public interface SymbolTable { 033 034 /** 035 * The list of symbols in the table 036 * @return the symbols 037 */ 038 public Map<String, Symbol> getSymbols(); 039 040 /** 041 * The list of symbols and their values in the table 042 * @return the name of the symbols and their values 043 */ 044 public Map<String, Object> getSymbolValues(); 045 046 /** 047 * Get the value of a symbol 048 * @param name the name 049 * @return the value 050 */ 051 public Object getValue(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 public 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 public 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 public 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 public 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 public 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 public 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 public 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 public Stack getStack(); 126 127 128 /** 129 * An enum that defines the types of initial value. 130 */ 131 public enum InitialValueType { 132 133 None(Bundle.getMessage("InitialValueType_None"), true), 134 Integer(Bundle.getMessage("InitialValueType_Integer"), true), 135 FloatingNumber(Bundle.getMessage("InitialValueType_FloatingNumber"), true), 136 String(Bundle.getMessage("InitialValueType_String"), true), 137 Array(Bundle.getMessage("InitialValueType_Array"), false), 138 Map(Bundle.getMessage("InitialValueType_Map"), false), 139 LocalVariable(Bundle.getMessage("InitialValueType_LocalVariable"), true), 140 Memory(Bundle.getMessage("InitialValueType_Memory"), true), 141 Reference(Bundle.getMessage("InitialValueType_Reference"), true), 142 Formula(Bundle.getMessage("InitialValueType_Formula"), true), 143 ScriptExpression(Bundle.getMessage("InitialValueType_ScriptExpression"), true), 144 ScriptFile(Bundle.getMessage("InitialValueType_ScriptFile"), true), 145 LogixNG_Table(Bundle.getMessage("InitialValueType_LogixNGTable"), true); 146 147 private final String _descr; 148 private final boolean _isValidAsParameter; 149 150 private InitialValueType(String descr, boolean isValidAsParameter) { 151 _descr = descr; 152 _isValidAsParameter = isValidAsParameter; 153 } 154 155 @Override 156 public String toString() { 157 return _descr; 158 } 159 160 public boolean isValidAsParameter() { 161 return _isValidAsParameter; 162 } 163 } 164 165 166 /** 167 * The definition of the symbol 168 */ 169 public interface Symbol { 170 171 /** 172 * The name of the symbol 173 * @return the name 174 */ 175 public String getName(); 176 177 /** 178 * The index on the stack for this symbol 179 * @return the index 180 */ 181 public int getIndex(); 182 183 } 184 185 186 /** 187 * Data for a variable. 188 */ 189 public static class VariableData { 190 191 public String _name; 192 public InitialValueType _initialValueType = InitialValueType.None; 193 public String _initialValueData; 194 195 public VariableData( 196 String name, 197 InitialValueType initialValueType, 198 String initialValueData) { 199 200 _name = name; 201 if (initialValueType != null) { 202 _initialValueType = initialValueType; 203 } 204 _initialValueData = initialValueData; 205 } 206 207 public VariableData(VariableData variableData) { 208 _name = variableData._name; 209 _initialValueType = variableData._initialValueType; 210 _initialValueData = variableData._initialValueData; 211 } 212 213 /** 214 * The name of the variable 215 * @return the name 216 */ 217 public String getName() { 218 return _name; 219 } 220 221 public InitialValueType getInitialValueType() { 222 return _initialValueType; 223 } 224 225 public String getInitialValueData() { 226 return _initialValueData; 227 } 228 229 } 230 231 /** 232 * Print a variable 233 * @param log the logger 234 * @param pad the padding 235 * @param name the name 236 * @param value the value 237 * @param expandArraysAndMaps true if arrays and maps should be expanded, false otherwise 238 * @param headerName header for the variable name 239 * @param headerValue header for the variable value 240 */ 241 @SuppressWarnings("unchecked") // Checked cast is not possible due to type erasure 242 @SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", justification="The code prints a complex variable, like a map, to the log") 243 public static void printVariable( 244 Logger log, 245 String pad, 246 String name, 247 Object value, 248 boolean expandArraysAndMaps, 249 String headerName, 250 String headerValue) { 251 252 if (expandArraysAndMaps && (value instanceof Map)) { 253 log.warn("{}{}: {},", pad, headerName, name); 254 var map = ((Map<? extends Object, ? extends Object>)value); 255 for (var entry : map.entrySet()) { 256 log.warn("{}{}{} -> {},", pad, pad, entry.getKey(), entry.getValue()); 257 } 258 } else if (expandArraysAndMaps && (value instanceof List)) { 259 log.warn("{}{}: {},", pad, headerName, name); 260 var list = ((List<? extends Object>)value); 261 for (int i=0; i < list.size(); i++) { 262 log.warn("{}{}{}: {},", pad, pad, i, list.get(i)); 263 } 264 } else { 265 log.warn("{}{}: {}, {}: {}", pad, headerName, name, headerValue, value); 266 } 267 } 268 269 private static Object runScriptExpression(String initialData) { 270 String script = 271 "import jmri\n" + 272 "variable.set(" + initialData + ")"; 273 274 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 275 276 Bindings bindings = new SimpleBindings(); 277 LogixNG_ScriptBindings.addScriptBindings(bindings); 278 279 var variable = new Reference<Object>(); 280 bindings.put("variable", variable); 281 282 try { 283 String theScript = String.format("import jmri%n") + script; 284 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 285 .eval(theScript, bindings); 286 } catch (ScriptException e) { 287 log.warn("cannot execute script", e); 288 return null; 289 } 290 return variable.get(); 291 } 292 293 private static Object runScriptFile(String initialData) { 294 295 JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault(); 296 297 Bindings bindings = new SimpleBindings(); 298 LogixNG_ScriptBindings.addScriptBindings(bindings); 299 300 var variable = new Reference<Object>(); 301 bindings.put("variable", variable); 302 303 try (InputStreamReader reader = new InputStreamReader( 304 new FileInputStream(jmri.util.FileUtil.getExternalFilename(initialData)), 305 StandardCharsets.UTF_8)) { 306 scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON) 307 .eval(reader, bindings); 308 } catch (IOException | ScriptException e) { 309 log.warn("cannot execute script \"{}\"", initialData, e); 310 return null; 311 } 312 return variable.get(); 313 } 314 315 private static Object copyLogixNG_Table(String initialData) { 316 317 NamedTable myTable = InstanceManager.getDefault(NamedTableManager.class) 318 .getNamedTable(initialData); 319 320 var myMap = new java.util.concurrent.ConcurrentHashMap<Object, Map<Object, Object>>(); 321 322 for (int row=1; row <= myTable.numRows(); row++) { 323 Object rowKey = myTable.getCell(row, 0); 324 var rowMap = new java.util.concurrent.ConcurrentHashMap<Object, Object>(); 325 326 for (int col=1; col <= myTable.numColumns(); col++) { 327 var columnKey = myTable.getCell(0, col); 328 var cellValue = myTable.getCell(row, col); 329 rowMap.put(columnKey, cellValue); 330 } 331 332 myMap.put(rowKey, rowMap); 333 } 334 335 return myMap; 336 } 337 338 public static Object getInitialValue( 339 InitialValueType initialType, 340 String initialData, 341 SymbolTable symbolTable, 342 Map<String, Symbol> symbols) 343 throws ParserException, JmriException { 344 345 switch (initialType) { 346 case None: 347 return null; 348 349 case Integer: 350 return Long.parseLong(initialData); 351 352 case FloatingNumber: 353 return Double.parseDouble(initialData); 354 355 case String: 356 return initialData; 357 358 case Array: 359 List<Object> array = new java.util.ArrayList<>(); 360 Object initialValue = array; 361 String initialValueData = initialData; 362 if ((initialValueData != null) && !initialValueData.isEmpty()) { 363 Object data = ""; 364 String[] parts = initialValueData.split(":", 2); 365 if (parts.length > 1) { 366 initialValueData = parts[0]; 367 if (Character.isDigit(parts[1].charAt(0))) { 368 try { 369 data = Long.parseLong(parts[1]); 370 } catch (NumberFormatException e) { 371 try { 372 data = Double.parseDouble(parts[1]); 373 } catch (NumberFormatException e2) { 374 throw new IllegalArgumentException("Data is not a number", e2); 375 } 376 } 377 } else if ((parts[1].charAt(0) == '"') && (parts[1].charAt(parts[1].length()-1) == '"')) { 378 data = parts[1].substring(1,parts[1].length()-1); 379 } else { 380 // Assume initial value is a local variable 381 data = symbolTable.getValue(parts[1]).toString(); 382 } 383 } 384 try { 385 int count; 386 if (Character.isDigit(initialValueData.charAt(0))) { 387 count = Integer.parseInt(initialValueData); 388 } else { 389 // Assume size is a local variable 390 count = Integer.parseInt(symbolTable.getValue(initialValueData).toString()); 391 } 392 for (int i=0; i < count; i++) array.add(data); 393 } catch (NumberFormatException e) { 394 throw new IllegalArgumentException("Initial capacity is not an integer", e); 395 } 396 } 397 return initialValue; 398 399 case Map: 400 return new java.util.HashMap<>(); 401 402 case LocalVariable: 403 return symbolTable.getValue(initialData); 404 405 case Memory: 406 Memory m = InstanceManager.getDefault(MemoryManager.class).getNamedBean(initialData); 407 if (m != null) return m.getValue(); 408 else return null; 409 410 case Reference: 411 if (ReferenceUtil.isReference(initialData)) { 412 return ReferenceUtil.getReference( 413 symbolTable, initialData); 414 } else { 415 log.error("\"{}\" is not a reference", initialData); 416 return null; 417 } 418 419 case Formula: 420 RecursiveDescentParser parser = createParser(symbols); 421 ExpressionNode expressionNode = parser.parseExpression( 422 initialData); 423 return expressionNode.calculate(symbolTable); 424 425 case ScriptExpression: 426 return runScriptExpression(initialData); 427 428 case ScriptFile: 429 return runScriptFile(initialData); 430 431 case LogixNG_Table: 432 return copyLogixNG_Table(initialData); 433 434 default: 435 log.error("definition._initialValueType has invalid value: {}", initialType.name()); 436 throw new IllegalArgumentException("definition._initialValueType has invalid value: " + initialType.name()); 437 } 438 } 439 440 private static RecursiveDescentParser createParser(Map<String, Symbol> symbols) 441 throws ParserException { 442 Map<String, Variable> variables = new HashMap<>(); 443 444 for (SymbolTable.Symbol symbol : Collections.unmodifiableMap(symbols).values()) { 445 variables.put(symbol.getName(), 446 new LocalVariableExpressionVariable(symbol.getName())); 447 } 448 449 return new RecursiveDescentParser(variables); 450 } 451 452 453 454 public static class SymbolNotFound extends IllegalArgumentException { 455 456 public SymbolNotFound(String message) { 457 super(message); 458 } 459 } 460 461 462 @SuppressFBWarnings(value="SLF4J_LOGGER_SHOULD_BE_PRIVATE", justification="Interfaces cannot have private fields") 463 final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SymbolTable.class); 464 465}