001package jmri.jmrit.logixng.implementation;
002
003import java.util.*;
004
005import javax.annotation.Nonnull;
006
007import jmri.*;
008import jmri.jmrit.logixng.*;
009import jmri.jmrit.logixng.Module;
010import jmri.jmrit.logixng.Stack;
011import jmri.jmrit.logixng.util.LogixNG_Thread;
012import jmri.util.ThreadingUtil;
013
014/**
015 * The default implementation of ConditionalNG.
016 *
017 * @author Daniel Bergqvist Copyright 2019
018 */
019public class DefaultConditionalNG extends AbstractBase
020        implements ConditionalNG, FemaleSocketListener {
021
022    private final LogixNG_Thread _thread;
023    private int _startupThreadId;
024    private Base _parent = null;
025    private String _socketSystemName = null;
026    private final FemaleDigitalActionSocket _femaleSocket;
027    private boolean _enabled = true;
028    private boolean _executeAtStartup = true;
029    private final ExecuteLock _executeLock = new ExecuteLock();
030    private boolean _runDelayed = true;
031    private final Stack _stack = new DefaultStack();
032    private SymbolTable _symbolTable;
033
034
035    public DefaultConditionalNG(String sys, String user)
036            throws BadUserNameException, BadSystemNameException  {
037        this(sys, user, LogixNG_Thread.DEFAULT_LOGIXNG_THREAD);
038    }
039
040    public DefaultConditionalNG(String sys, String user, int threadID)
041            throws BadUserNameException, BadSystemNameException  {
042        super(sys, user);
043
044        _startupThreadId = threadID;
045        _thread = LogixNG_Thread.getThread(threadID);
046        _thread.setThreadInUse();
047
048        // Do this test here to ensure all the tests are using correct system names
049        Manager.NameValidity isNameValid = InstanceManager.getDefault(ConditionalNG_Manager.class).validSystemNameFormat(mSystemName);
050        if (isNameValid != Manager.NameValidity.VALID) {
051            throw new IllegalArgumentException("system name is not valid");
052        }
053        _femaleSocket = new DefaultFemaleDigitalActionSocket(this, this, "A");
054    }
055
056    /** {@inheritDoc} */
057    @Override
058    public LogixNG_Thread getCurrentThread() {
059        return _thread;
060    }
061
062    /** {@inheritDoc} */
063    @Override
064    public int getStartupThreadId() {
065        return _startupThreadId;
066    }
067
068    /** {@inheritDoc} */
069    @Override
070    public void setStartupThreadId(int threadId) {
071        int oldStartupThreadId = _startupThreadId;
072        _startupThreadId = threadId;
073        firePropertyChange("Thread", oldStartupThreadId, _startupThreadId);
074    }
075
076    /** {@inheritDoc} */
077    @Override
078    public Base getParent() {
079        return _parent;
080    }
081
082    /** {@inheritDoc} */
083    @Override
084    public void setParent(Base parent) {
085        _parent = parent;
086
087        if (isActive()) registerListeners();
088        else unregisterListeners();
089    }
090
091    /** {@inheritDoc} */
092    @Override
093    public FemaleDigitalActionSocket getFemaleSocket() {
094        return _femaleSocket;
095    }
096
097    /** {@inheritDoc} */
098    @Override
099    public void setRunDelayed(boolean value) {
100        _runDelayed = value;
101    }
102
103    /** {@inheritDoc} */
104    @Override
105    public boolean getRunDelayed() {
106        return _runDelayed;
107    }
108
109    private void runOnLogixNG_Thread(
110            @Nonnull ThreadingUtil.ThreadAction ta,
111            boolean allowRunDelayed) {
112
113        if (_runDelayed && allowRunDelayed) {
114            _thread.runOnLogixNGEventually(ta);
115        } else {
116            _thread.runOnLogixNG(ta);
117        }
118    }
119
120    /** {@inheritDoc} */
121    @Override
122    public void execute() {
123        if (_executeAtStartup || !getLogixNG().isStartup()) {
124            if (_executeLock.once()) {
125                runOnLogixNG_Thread(new ExecuteTask(this, _executeLock, null), true);
126            }
127        }
128    }
129
130    /** {@inheritDoc} */
131    @Override
132    public void execute(boolean allowRunDelayed) {
133        if (_executeLock.once()) {
134            runOnLogixNG_Thread(new ExecuteTask(this, _executeLock, null), allowRunDelayed);
135        }
136    }
137
138    /** {@inheritDoc} */
139    @Override
140    public void execute(FemaleDigitalActionSocket socket) {
141        runOnLogixNG_Thread(() -> {internalExecute(this, socket);}, true);
142    }
143
144    /**
145     * Executes a LogixNG Module.
146     * @param module      The module to be executed
147     * @param parameters  The parameters
148     * @throws IllegalArgumentException when needed
149     */
150    public static void executeModule(Module module, Map<String, Object> parameters)
151            throws IllegalArgumentException {
152
153        if (module == null) {
154            throw new IllegalArgumentException("The parameter \"module\" is null");
155        }
156        if (!(module.getRootSocket() instanceof DefaultFemaleDigitalActionSocket)) {
157            throw new IllegalArgumentException("The module " + module.getDisplayName() + " is not a DigitalActionModule");
158        }
159        if (parameters == null) {
160            throw new IllegalArgumentException("The parameter \"parameters\" is null");
161        }
162
163        LogixNG_Thread thread = LogixNG_Thread.getThread(LogixNG_Thread.DEFAULT_LOGIXNG_THREAD);
164        ConditionalNG conditionalNG = new DefaultConditionalNG("IQC0000000", null);
165        InternalFemaleSocket socket = new InternalFemaleSocket(conditionalNG, module, parameters);
166        thread.runOnLogixNGEventually(() -> { internalExecute(conditionalNG, socket); });
167    }
168
169    private static class InternalFemaleSocket extends DefaultFemaleDigitalActionSocket {
170
171        private final ConditionalNG _conditionalNG;
172        private final Module _module;
173        private final Map<String, Object> _parameters;
174
175        public InternalFemaleSocket(ConditionalNG conditionalNG, Module module, Map<String, Object> parameters) {
176            super(null, new FemaleSocketListener(){
177                @Override
178                public void connected(FemaleSocket socket) {
179                    // Do nothing
180                }
181
182                @Override
183                public void disconnected(FemaleSocket socket) {
184                    // Do nothing
185                }
186            }, "A");
187            _conditionalNG = conditionalNG;
188            _module = module;
189            _parameters = parameters;
190        }
191
192        @Override
193        public void execute() throws JmriException {
194            FemaleSocket socket = _module.getRootSocket();
195            if (!(socket instanceof DefaultFemaleDigitalActionSocket)) {
196                throw new IllegalArgumentException("The module " + _module.getDisplayName() + " is not a DigitalActionModule");
197            }
198
199            synchronized(this) {
200                SymbolTable oldSymbolTable = _conditionalNG.getSymbolTable();
201                DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(_conditionalNG);
202                List<Module.ParameterData> _parameterData = new ArrayList<>();
203                for (Module.Parameter p : _module.getParameters()) {
204                    _parameterData.add(new Module.ParameterData(
205                            p.getName(), SymbolTable.InitialValueType.None, "",
206                            Module.ReturnValueType.None, ""));
207                }
208                newSymbolTable.createSymbols(_conditionalNG.getSymbolTable(), _parameterData);
209                for (var entry : _parameters.entrySet()) {
210                    newSymbolTable.setValue(entry.getKey(), entry.getValue());
211                }
212                _conditionalNG.setSymbolTable(newSymbolTable);
213
214                ((DefaultFemaleDigitalActionSocket)socket).execute();
215                _conditionalNG.setSymbolTable(oldSymbolTable);
216            }
217        }
218    }
219
220    private static void internalExecute(ConditionalNG conditionalNG, FemaleDigitalActionSocket femaleSocket) {
221        if (conditionalNG.isEnabled()) {
222            DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(conditionalNG);
223
224            try {
225                conditionalNG.setCurrentConditionalNG(conditionalNG);
226
227                conditionalNG.setSymbolTable(newSymbolTable);
228
229                LogixNG logixNG = conditionalNG.getLogixNG();
230                InlineLogixNG inlineLogixNG = null;
231                if (logixNG != null) {
232                    inlineLogixNG = logixNG.getInlineLogixNG();
233                }
234                if (inlineLogixNG != null) {
235                    List<SymbolTable.VariableData> localVariables = new ArrayList<>();
236                    localVariables.add(new SymbolTable.VariableData(
237                            "__InlineLogixNG__", SymbolTable.InitialValueType.String,
238                            inlineLogixNG.getNameString()));
239//                    localVariables.add(new SymbolTable.VariableData(
240//                            "__PositionableId__", SymbolTable.InitialValueType.String,
241//                            inlineLogixNG.getId()));
242                    localVariables.add(new SymbolTable.VariableData(
243                            "__Editor__", SymbolTable.InitialValueType.String,
244                            inlineLogixNG.getEditorName()));
245                    newSymbolTable.createSymbols(localVariables);
246                }
247
248                if (femaleSocket != null) {
249                    femaleSocket.execute();
250                } else {
251                    conditionalNG.getFemaleSocket().execute();
252                }
253            } catch (AbortConditionalNG_IgnoreException | ReturnException | ExitException e) {
254                // A AbortConditionalNG_IgnoreException should be ignored.
255                // A Return action in a ConditionalNG causes a ReturnException so this is okay.
256                // An Exit action in a ConditionalNG causes a ExitException so this is okay.
257            } catch (PassThruException e) {
258                // This happens due to a a Break action or a Continue action that isn't handled.
259                log.info("ConditionalNG {} was aborted during execute: {}",
260                        conditionalNG.getSystemName(), e.getCause(), e.getCause());
261            } catch (AbortConditionalNGExecutionException e) {
262                if (InstanceManager.getDefault(LogixNGPreferences.class).getShowSystemNameInException()) {
263                    log.warn("ConditionalNG {} was aborted during execute in the item {}: {}",
264                            conditionalNG.getSystemName(), e.getMaleSocket().getSystemName(), e.getCause(), e.getCause());
265                } else {
266                    log.warn("ConditionalNG {} was aborted during execute: {}",
267                            conditionalNG.getSystemName(), e.getCause(), e.getCause());
268                }
269            } catch (JmriException | RuntimeException e) {
270//                LoggingUtil.warnOnce(log, "ConditionalNG {} got an exception during execute: {}",
271//                        conditionalNG.getSystemName(), e, e);
272                log.warn("ConditionalNG {} got an exception during execute: {}",
273                        conditionalNG.getSystemName(), e, e);
274            }
275
276            conditionalNG.setSymbolTable(newSymbolTable.getPrevSymbolTable());
277        }
278    }
279
280    private static class ExecuteTask implements ThreadingUtil.ThreadAction {
281
282        private final ConditionalNG _conditionalNG;
283        private final ExecuteLock _executeLock;
284        private final FemaleDigitalActionSocket _localFemaleSocket;
285
286        public ExecuteTask(ConditionalNG conditionalNG, ExecuteLock executeLock, FemaleDigitalActionSocket femaleSocket) {
287            _conditionalNG = conditionalNG;
288            _executeLock = executeLock;
289            _localFemaleSocket = femaleSocket;
290        }
291
292        @Override
293        public void run() {
294            while (_executeLock.loop()) {
295                internalExecute(_conditionalNG, _localFemaleSocket);
296            }
297        }
298
299    }
300
301    /**
302     * Set the current ConditionalNG.
303     * @param conditionalNG the current ConditionalNG
304     */
305    @Override
306    public void setCurrentConditionalNG(ConditionalNG conditionalNG) {
307        if (this != conditionalNG) {
308            throw new UnsupportedOperationException("The new conditionalNG must be the same as myself");
309        }
310        for (Module m : InstanceManager.getDefault(ModuleManager.class).getNamedBeanSet()) {
311            m.setCurrentConditionalNG(conditionalNG);
312        }
313    }
314
315    /** {@inheritDoc} */
316    @Override
317    public Stack getStack() {
318        return _stack;
319    }
320
321    /** {@inheritDoc} */
322    @Override
323    public SymbolTable getSymbolTable() {
324        return _symbolTable;
325    }
326
327    /** {@inheritDoc} */
328    @Override
329    public void setSymbolTable(SymbolTable symbolTable) {
330        _symbolTable = symbolTable;
331    }
332
333    @Override
334    public String getBeanType() {
335        return Bundle.getMessage("BeanNameConditionalNG");
336    }
337
338    @Override
339    public void setState(int s) throws JmriException {
340        log.warn("Unexpected call to setState in DefaultConditionalNG.");  // NOI18N
341    }
342
343    @Override
344    public int getState() {
345        log.warn("Unexpected call to getState in DefaultConditionalNG.");  // NOI18N
346        return UNKNOWN;
347    }
348
349    @Override
350    public void connected(FemaleSocket socket) {
351        _socketSystemName = socket.getConnectedSocket().getSystemName();
352    }
353
354    @Override
355    public void disconnected(FemaleSocket socket) {
356        _socketSystemName = null;
357    }
358
359    @Override
360    public String getShortDescription(Locale locale) {
361        return "ConditionalNG: "+getDisplayName();
362    }
363
364    @Override
365    public String getLongDescription(Locale locale) {
366        if (_thread.getThreadId() != LogixNG_Thread.DEFAULT_LOGIXNG_THREAD) {
367            return "ConditionalNG: "+getDisplayName() + " on thread " + _thread.getThreadName();
368        }
369        return "ConditionalNG: "+getDisplayName();
370    }
371
372    @Override
373    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
374        if (index != 0) {
375            throw new IllegalArgumentException(
376                    String.format("index has invalid value: %d", index));
377        }
378
379        return _femaleSocket;
380    }
381
382    @Override
383    public int getChildCount() {
384        return 1;
385    }
386
387    @Override
388    public Category getCategory() {
389        throw new UnsupportedOperationException("Not supported.");
390    }
391
392    @Override
393    public void setSocketSystemName(String systemName) {
394        if ((systemName == null) || (!systemName.equals(_socketSystemName))) {
395            _femaleSocket.disconnect();
396        }
397        _socketSystemName = systemName;
398    }
399
400    @Override
401    public String getSocketSystemName() {
402        return _socketSystemName;
403    }
404
405    /** {@inheritDoc} */
406    @Override
407    final public void setup() {
408        if (!_femaleSocket.isConnected()
409                || !_femaleSocket.getConnectedSocket().getSystemName()
410                        .equals(_socketSystemName)) {
411
412            _femaleSocket.disconnect();
413
414            if (_socketSystemName != null) {
415                try {
416                    MaleSocket maleSocket =
417                            InstanceManager.getDefault(DigitalActionManager.class)
418                                    .getBySystemName(_socketSystemName);
419                    if (maleSocket != null) {
420                        _femaleSocket.connect(maleSocket);
421                        maleSocket.setup();
422                    } else {
423                        log.error("digital action is not found: {}", _socketSystemName);
424                    }
425                } catch (SocketAlreadyConnectedException ex) {
426                    // This shouldn't happen and is a runtime error if it does.
427                    throw new RuntimeException("socket is already connected");
428                }
429            }
430        } else {
431            _femaleSocket.setup();
432        }
433    }
434
435    /** {@inheritDoc} */
436    @Override
437    final public void disposeMe() {
438        _femaleSocket.dispose();
439    }
440
441    /** {@inheritDoc} */
442    @Override
443    public void setEnabled(boolean enable) {
444        _enabled = enable;
445        if (isActive()) {
446            LogixNG logixNG = getLogixNG();
447            if ((logixNG != null) && logixNG.isActive()) {
448                registerListeners();
449                if (_executeAtStartup) {
450                    execute();
451                }
452            }
453        } else {
454            unregisterListeners();
455        }
456    }
457
458    /** {@inheritDoc} */
459    @Override
460    public boolean isEnabled() {
461        return _enabled;
462    }
463
464    /** {@inheritDoc} */
465    @Override
466    public void setExecuteAtStartup(boolean value) {
467        _executeAtStartup = value;
468    }
469
470    /** {@inheritDoc} */
471    @Override
472    public boolean isExecuteAtStartup() {
473        return _executeAtStartup;
474    }
475
476    /** {@inheritDoc} */
477    @Override
478    public synchronized boolean isListenersRegistered() {
479        return _listenersAreRegistered;
480    }
481
482    /** {@inheritDoc} */
483    @Override
484    public synchronized void registerListenersForThisClass() {
485        _listenersAreRegistered = true;
486    }
487
488    /** {@inheritDoc} */
489    @Override
490    public synchronized void unregisterListenersForThisClass() {
491        _listenersAreRegistered = false;
492    }
493
494    @Override
495    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) {
496        throw new UnsupportedOperationException("Not supported yet.");
497    }
498
499    @Override
500    public boolean existsInTree() {
501        return true;
502    }
503
504    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultConditionalNG.class);
505
506}