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}