001package jmri.jmrit.logixng;
002
003import java.io.*;
004import java.nio.charset.StandardCharsets;
005import java.util.*;
006
007import javax.script.*;
008
009import org.slf4j.Logger;
010
011import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
012import jmri.*;
013import jmri.jmrit.logixng.Stack.ValueAndType;
014import jmri.jmrit.logixng.util.ReferenceUtil;
015import jmri.jmrit.logixng.util.parser.*;
016import jmri.script.JmriScriptEngineManager;
017import jmri.util.TypeConversionUtil;
018
019/**
020 * A symbol table
021 *
022 * @author Daniel Bergqvist 2020
023 */
024public interface SymbolTable {
025
026    /**
027     * The list of symbols in the table
028     * @return the symbols
029     */
030    Map<String, Symbol> getSymbols();
031
032    /**
033     * The list of symbols and their values in the table
034     * @return the name of the symbols and their values
035     */
036    Map<String, Object> getSymbolValues();
037
038    /**
039     * Get the value of a symbol
040     * @param name the name
041     * @return the value
042     */
043    Object getValue(String name);
044
045    /**
046     * Get the value and type of a symbol.
047     * This method does not lookup global variables.
048     * @param name the name
049     * @return the value and type
050     */
051    ValueAndType getValueAndType(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    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    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    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    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    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    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    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    Stack getStack();
126
127
128    /**
129     * An enum that defines the types of initial value.
130     */
131    enum InitialValueType {
132
133        None(Bundle.getMessage("InitialValueType_None"), true),
134        Boolean(Bundle.getMessage("InitialValueType_Boolean"), true),
135        Integer(Bundle.getMessage("InitialValueType_Integer"), true),
136        FloatingNumber(Bundle.getMessage("InitialValueType_FloatingNumber"), true),
137        String(Bundle.getMessage("InitialValueType_String"), true),
138        Array(Bundle.getMessage("InitialValueType_Array"), false),
139        Map(Bundle.getMessage("InitialValueType_Map"), false),
140        LocalVariable(Bundle.getMessage("InitialValueType_LocalVariable"), true),
141        Memory(Bundle.getMessage("InitialValueType_Memory"), true),
142        Reference(Bundle.getMessage("InitialValueType_Reference"), true),
143        Formula(Bundle.getMessage("InitialValueType_Formula"), true),
144        ScriptExpression(Bundle.getMessage("InitialValueType_ScriptExpression"), true),
145        ScriptFile(Bundle.getMessage("InitialValueType_ScriptFile"), true),
146        LogixNG_Table(Bundle.getMessage("InitialValueType_LogixNGTable"), true),
147
148        // This can't be selected by the user. It's only used internally.
149        Object(Bundle.getMessage("InitialValueType_None"), false, false);
150
151
152        private final String _descr;
153        private final boolean _isValidAsParameter;
154        private final boolean _isVisible;
155
156        private InitialValueType(String descr, boolean isValidAsParameter) {
157            this(descr, isValidAsParameter, true);
158        }
159
160        private InitialValueType(String descr, boolean isValidAsParameter, boolean isVisible) {
161            _descr = descr;
162            _isValidAsParameter = isValidAsParameter;
163            _isVisible = isVisible;
164        }
165
166        @Override
167        public String toString() {
168            return _descr;
169        }
170
171        public boolean isValidAsParameter() {
172            return _isValidAsParameter;
173        }
174
175        public boolean isVisible() {
176            return _isVisible;
177        }
178    }
179
180
181    /**
182     * The definition of the symbol
183     */
184    interface Symbol {
185
186        /**
187         * The name of the symbol
188         * @return the name
189         */
190        String getName();
191
192        /**
193         * The index on the stack for this symbol
194         * @return the index
195         */
196        int getIndex();
197
198    }
199
200
201    /**
202     * Data for a variable.
203     */
204    static class VariableData {
205
206        public String _name;
207        public InitialValueType _initialValueType = InitialValueType.None;
208        public String _initialValueData;
209
210        public VariableData(
211                String name,
212                InitialValueType initialValueType,
213                String initialValueData) {
214
215            _name = name;
216            if (initialValueType != null) {
217                _initialValueType = initialValueType;
218            }
219            _initialValueData = initialValueData;
220        }
221
222        public VariableData(VariableData variableData) {
223            _name = variableData._name;
224            _initialValueType = variableData._initialValueType;
225            _initialValueData = variableData._initialValueData;
226        }
227
228        /**
229         * The name of the variable
230         * @return the name
231         */
232        public String getName() {
233            return _name;
234        }
235
236        public InitialValueType getInitialValueType() {
237            return _initialValueType;
238        }
239
240        public String getInitialValueData() {
241            return _initialValueData;
242        }
243
244    }
245
246    /**
247     * Print a variable
248     * @param log          the logger
249     * @param pad          the padding
250     * @param name         the name
251     * @param value        the value
252     * @param expandArraysAndMaps   true if arrays and maps should be expanded, false otherwise
253     * @param showClassName         true if class name should be shown
254     * @param headerName   header for the variable name
255     * @param headerValue  header for the variable value
256     */
257    @SuppressWarnings("unchecked")  // Checked cast is not possible due to type erasure
258    @SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", justification="The code prints a complex variable, like a map, to the log")
259    static void printVariable(
260            Logger log,
261            String pad,
262            String name,
263            Object value,
264            boolean expandArraysAndMaps,
265            boolean showClassName,
266            String headerName,
267            String headerValue) {
268
269        if (expandArraysAndMaps && (value instanceof Map)) {
270            log.warn("{}{}: {},", pad, headerName, name);
271            var map = ((Map<? extends Object, ? extends Object>)value);
272            for (var entry : map.entrySet()) {
273                String className = showClassName && entry.getValue() != null
274                        ? ", " + entry.getValue().getClass().getName()
275                        : "";
276                log.warn("{}{}{} -> {}{},", pad, pad, entry.getKey(), entry.getValue(), className);
277            }
278        } else if (expandArraysAndMaps && (value instanceof List)) {
279            log.warn("{}{}: {},", pad, headerName, name);
280            var list = ((List<? extends Object>)value);
281            for (int i=0; i < list.size(); i++) {
282                Object val = list.get(i);
283                String className = showClassName && val != null
284                        ? ", " + val.getClass().getName()
285                        : "";
286                log.warn("{}{}{}: {}{},", pad, pad, i, val, className);
287            }
288        } else  {
289            String className = showClassName && value != null
290                    ? ", " + value.getClass().getName()
291                    : "";
292            if (value instanceof NamedBean) {
293                // Show display name instead of system name
294                value = ((NamedBean)value).getDisplayName();
295            }
296            log.warn("{}{}: {}, {}: {}{}", pad, headerName, name, headerValue, value, className);
297        }
298    }
299
300    private static Object runScriptExpression(SymbolTable symbolTable, String initialData) {
301        String script =
302                "import jmri\n" +
303                "variable.set(" + initialData + ")";
304
305        JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault();
306
307        Bindings bindings = new SimpleBindings();
308        LogixNG_ScriptBindings.addScriptBindings(bindings);
309
310        var variable = new Reference<Object>();
311        bindings.put("variable", variable);
312
313        bindings.put("symbolTable", symbolTable);    // Give the script access to the local variables in the symbol table
314
315        try {
316            String theScript = String.format("import jmri%n") + script;
317            scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON)
318                    .eval(theScript, bindings);
319        } catch (ScriptException e) {
320            log.warn("cannot execute script", e);
321            return null;
322        }
323        return variable.get();
324    }
325
326    private static Object runScriptFile(SymbolTable symbolTable, String initialData) {
327
328        JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault();
329
330        Bindings bindings = new SimpleBindings();
331        LogixNG_ScriptBindings.addScriptBindings(bindings);
332
333        var variable = new Reference<Object>();
334        bindings.put("variable", variable);
335
336        bindings.put("symbolTable", symbolTable);    // Give the script access to the local variables in the symbol table
337
338        try (InputStreamReader reader = new InputStreamReader(
339                new FileInputStream(jmri.util.FileUtil.getExternalFilename(initialData)),
340                StandardCharsets.UTF_8)) {
341            scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON)
342                    .eval(reader, bindings);
343        } catch (IOException | ScriptException e) {
344            log.warn("cannot execute script \"{}\"", initialData, e);
345            return null;
346        }
347        return variable.get();
348    }
349
350    private static Object copyLogixNG_Table(String initialData) {
351
352        NamedTable myTable = InstanceManager.getDefault(NamedTableManager.class)
353                .getNamedTable(initialData);
354
355        var myMap = new java.util.concurrent.ConcurrentHashMap<Object, Map<Object, Object>>();
356
357        for (int row=1; row <= myTable.numRows(); row++) {
358            Object rowKey = myTable.getCell(row, 0);
359            var rowMap = new java.util.concurrent.ConcurrentHashMap<Object, Object>();
360
361            for (int col=1; col <= myTable.numColumns(); col++) {
362                var columnKey = myTable.getCell(0, col);
363                var cellValue = myTable.getCell(row, col);
364                rowMap.put(columnKey, cellValue);
365            }
366
367            myMap.put(rowKey, rowMap);
368        }
369
370        return myMap;
371    }
372
373
374    enum Type {
375        Global("global variable"),
376        Local("local variable"),
377        Parameter("parameter");
378
379        private final String _descr;
380
381        private Type(String descr) {
382            _descr = descr;
383        }
384    }
385
386
387    private static void validateValue(Type type, String name, String initialData, String descr) {
388        if (initialData == null) {
389            throw new IllegalArgumentException(String.format("Initial data is null for %s \"%s\". Can't set value %s.", type._descr, name, descr));
390        }
391        if (initialData.isBlank()) {
392            throw new IllegalArgumentException(String.format("Initial data is empty string for %s \"%s\". Can't set value %s.", type._descr, name, descr));
393        }
394    }
395
396    static Object getInitialValue(
397            Type type,
398            String name,
399            InitialValueType initialType,
400            String initialData,
401            SymbolTable symbolTable,
402            Map<String, Symbol> symbols)
403            throws ParserException, JmriException {
404
405        switch (initialType) {
406            case None:
407                return null;
408
409            case Boolean:
410                validateValue(type, name, initialData, "to boolean");
411                return TypeConversionUtil.convertToBoolean(initialData, true);
412
413            case Integer:
414                validateValue(type, name, initialData, "to integer");
415                return Long.valueOf(initialData);
416
417            case FloatingNumber:
418                validateValue(type, name, initialData, "to floating number");
419                return Double.valueOf(initialData);
420
421            case String:
422                return initialData;
423
424            case Array:
425                List<Object> array = new java.util.ArrayList<>();
426                Object initialValue = array;
427                String initialValueData = initialData;
428                if ((initialValueData != null) && !initialValueData.isEmpty()) {
429                    Object data = "";
430                    String[] parts = initialValueData.split(":", 2);
431                    if (parts.length > 1) {
432                        initialValueData = parts[0];
433                        if (Character.isDigit(parts[1].charAt(0))) {
434                            try {
435                                data = Long.valueOf(parts[1]);
436                            } catch (NumberFormatException e) {
437                                try {
438                                    data = Double.valueOf(parts[1]);
439                                } catch (NumberFormatException e2) {
440                                    throw new IllegalArgumentException("Data is not a number", e2);
441                                }
442                            }
443                        } else if ((parts[1].charAt(0) == '"') && (parts[1].charAt(parts[1].length()-1) == '"')) {
444                            data = parts[1].substring(1,parts[1].length()-1);
445                        } else {
446                            // Assume initial value is a local variable
447                            data = symbolTable.getValue(parts[1]).toString();
448                        }
449                    }
450                    try {
451                        int count;
452                        if (Character.isDigit(initialValueData.charAt(0))) {
453                            count = Integer.parseInt(initialValueData);
454                        } else {
455                            // Assume size is a local variable
456                            count = Integer.parseInt(symbolTable.getValue(initialValueData).toString());
457                        }
458                        for (int i=0; i < count; i++) array.add(data);
459                    } catch (NumberFormatException e) {
460                        throw new IllegalArgumentException("Initial capacity is not an integer", e);
461                    }
462                }
463                return initialValue;
464
465            case Map:
466                return new java.util.HashMap<>();
467
468            case LocalVariable:
469                validateValue(type, name, initialData, "from local variable");
470                return symbolTable.getValue(initialData);
471
472            case Memory:
473                validateValue(type, name, initialData, "from memory");
474                Memory m = InstanceManager.getDefault(MemoryManager.class).getNamedBean(initialData);
475                if (m != null) return m.getValue();
476                else return null;
477
478            case Reference:
479                validateValue(type, name, initialData, "from reference");
480                if (ReferenceUtil.isReference(initialData)) {
481                    return ReferenceUtil.getReference(
482                            symbolTable, initialData);
483                } else {
484                    log.error("\"{}\" is not a reference", initialData);
485                    return null;
486                }
487
488            case Formula:
489                validateValue(type, name, initialData, "from formula");
490                RecursiveDescentParser parser = createParser(symbols);
491                ExpressionNode expressionNode = parser.parseExpression(
492                        initialData);
493                return expressionNode.calculate(symbolTable);
494
495            case ScriptExpression:
496                validateValue(type, name, initialData, "from script expression");
497                return runScriptExpression(symbolTable, initialData);
498
499            case ScriptFile:
500                validateValue(type, name, initialData, "from script file");
501                return runScriptFile(symbolTable, initialData);
502
503            case LogixNG_Table:
504                validateValue(type, name, initialData, "from logixng table");
505                return copyLogixNG_Table(initialData);
506
507            case Object:
508                return initialData;
509
510            default:
511                log.error("definition._initialValueType has invalid value: {}", initialType.name());
512                throw new IllegalArgumentException("definition._initialValueType has invalid value: " + initialType.name());
513        }
514    }
515
516    private static RecursiveDescentParser createParser(Map<String, Symbol> symbols)
517            throws ParserException {
518        Map<String, Variable> variables = new HashMap<>();
519
520        for (SymbolTable.Symbol symbol : Collections.unmodifiableMap(symbols).values()) {
521            variables.put(symbol.getName(),
522                    new LocalVariableExpressionVariable(symbol.getName()));
523        }
524
525        return new RecursiveDescentParser(variables);
526    }
527
528    /**
529     * Validates that the value can be assigned to a local or global variable
530     * of the specified type if strict typing is enforced. The caller must check
531     * first if this method should be called or not.
532     * @param type the type
533     * @param oldValue the old value
534     * @param newValue the new value
535     * @return the value to assign. It might be converted if needed.
536     * @throws NumberFormatException when needed
537     */
538    public static Object validateStrictTyping(InitialValueType type, Object oldValue, Object newValue)
539            throws NumberFormatException {
540
541        switch (type) {
542            case None:
543                return newValue;
544            case Boolean:
545                return TypeConversionUtil.convertToBoolean(newValue, true);
546            case Integer:
547                return TypeConversionUtil.convertToLong(newValue, true, true);
548            case FloatingNumber:
549                return TypeConversionUtil.convertToDouble(newValue, false, true, true);
550            case String:
551                if (newValue == null) {
552                    return null;
553                }
554                return newValue.toString();
555            default:
556                if (oldValue == null) {
557                    return newValue;
558                }
559                throw new IllegalArgumentException(String.format("A variable of type %s cannot change its value", type._descr));
560        }
561    }
562
563
564    static class SymbolNotFound extends IllegalArgumentException {
565
566        public SymbolNotFound(String message) {
567            super(message);
568        }
569    }
570
571
572    @SuppressFBWarnings(value="SLF4J_LOGGER_SHOULD_BE_PRIVATE", justification="Interfaces cannot have private fields")
573    org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SymbolTable.class);
574
575}