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}