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}