001package jmri.jmrit.logixng.actions;
002
003import jmri.jmrit.logixng.NamedBeanType;
004
005import java.beans.*;
006import java.util.*;
007
008import javax.annotation.Nonnull;
009
010import jmri.*;
011import jmri.jmrit.logixng.*;
012import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
013
014import net.jcip.annotations.GuardedBy;
015
016/**
017 * This action listens on some beans and runs the ConditionalNG on property change.
018 *
019 * @author Daniel Bergqvist Copyright 2022
020 */
021public class ActionListenOnBeansLocalVariable extends AbstractDigitalAction
022        implements FemaleSocketListener, PropertyChangeListener, VetoableChangeListener {
023
024    private NamedBeanType _namedBeanType = NamedBeanType.Light;
025    private boolean _listenOnAllProperties = false;
026    private String _localVariableBeanToListenOn;
027    private String _localVariableNamedBean;
028    private String _localVariableEvent;
029    private String _localVariableNewValue;
030    private final Map<NamedBean, String> _namedBeansEntries = new HashMap<>();
031    private final InternalFemaleSocket _internalSocket = new InternalFemaleSocket();
032    private String _executeSocketSystemName;
033    private final FemaleDigitalActionSocket _executeSocket;
034
035    @GuardedBy("this")
036    private final Deque<PropertyChangeEvent> _eventQueue = new ArrayDeque<>();
037
038
039    public ActionListenOnBeansLocalVariable(String sys, String user)
040            throws BadUserNameException, BadSystemNameException {
041        super(sys, user);
042        _executeSocket = InstanceManager.getDefault(DigitalActionManager.class)
043                .createFemaleSocket(this, this, Bundle.getMessage("ShowDialog_SocketExecute"));
044    }
045
046    @Override
047    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames)
048            throws JmriException {
049
050        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
051        String sysName = systemNames.get(getSystemName());
052        String userName = userNames.get(getSystemName());
053        if (sysName == null) sysName = manager.getAutoSystemName();
054        ActionListenOnBeansLocalVariable copy = new ActionListenOnBeansLocalVariable(sysName, userName);
055        copy.setComment(getComment());
056        copy.setNamedBeanType(_namedBeanType);
057
058        copy.setLocalVariableBeanToListenOn(_localVariableBeanToListenOn);
059        copy.setLocalVariableNamedBean(_localVariableNamedBean);
060        copy.setLocalVariableEvent(_localVariableEvent);
061        copy.setLocalVariableNewValue(_localVariableNewValue);
062
063        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
064    }
065
066    /**
067     * Get the type of the named beans
068     * @return the type of named beans
069     */
070    public NamedBeanType getNamedBeanType() {
071        return _namedBeanType;
072    }
073
074    /**
075     * Set the type of the named beans
076     * @param namedBeanType the type of the named beans
077     */
078    public void setNamedBeanType(@Nonnull NamedBeanType namedBeanType) {
079        if (namedBeanType == null) {
080            throw new IllegalArgumentException("namedBeanType must not be null");
081        }
082        _namedBeanType = namedBeanType;
083    }
084
085    public boolean getListenOnAllProperties() {
086        return _listenOnAllProperties;
087    }
088
089    public void setListenOnAllProperties(boolean listenOnAllProperties) {
090        _listenOnAllProperties = listenOnAllProperties;
091    }
092
093    public void setLocalVariableBeanToListenOn(String localVariableBeanToListenOn) {
094        if ((localVariableBeanToListenOn != null) && (!localVariableBeanToListenOn.isEmpty())) {
095            this._localVariableBeanToListenOn = localVariableBeanToListenOn;
096        } else {
097            this._localVariableBeanToListenOn = null;
098        }
099    }
100
101    public String getLocalVariableBeanToListenOn() {
102        return _localVariableBeanToListenOn;
103    }
104
105    public void setLocalVariableNamedBean(String localVariableNamedBean) {
106        if ((localVariableNamedBean != null) && (!localVariableNamedBean.isEmpty())) {
107            this._localVariableNamedBean = localVariableNamedBean;
108        } else {
109            this._localVariableNamedBean = null;
110        }
111    }
112
113    public String getLocalVariableNamedBean() {
114        return _localVariableNamedBean;
115    }
116
117    public void setLocalVariableEvent(String localVariableEvent) {
118        if ((localVariableEvent != null) && (!localVariableEvent.isEmpty())) {
119            this._localVariableEvent = localVariableEvent;
120        } else {
121            this._localVariableEvent = null;
122        }
123    }
124
125    public String getLocalVariableEvent() {
126        return _localVariableEvent;
127    }
128
129    public void setLocalVariableNewValue(String localVariableNewValue) {
130        if ((localVariableNewValue != null) && (!localVariableNewValue.isEmpty())) {
131            this._localVariableNewValue = localVariableNewValue;
132        } else {
133            this._localVariableNewValue = null;
134        }
135    }
136
137    public String getLocalVariableNewValue() {
138        return _localVariableNewValue;
139    }
140
141    public FemaleDigitalActionSocket getExecuteSocket() {
142        return _executeSocket;
143    }
144
145    public String getExecuteSocketSystemName() {
146        return _executeSocketSystemName;
147    }
148
149    public void setExecuteSocketSystemName(String systemName) {
150        _executeSocketSystemName = systemName;
151    }
152
153    /** {@inheritDoc} */
154    @Override
155    public Category getCategory() {
156        return Category.OTHER;
157    }
158
159    /** {@inheritDoc} */
160    @Override
161    public void execute() {
162        if (_localVariableBeanToListenOn != null
163                && !_localVariableBeanToListenOn.isBlank()) {
164
165            ConditionalNG conditionalNG = getConditionalNG();
166            _internalSocket._conditionalNG = conditionalNG;
167            _internalSocket._newSymbolTable = new DefaultSymbolTable(conditionalNG.getSymbolTable());
168
169            SymbolTable symbolTable = conditionalNG.getSymbolTable();
170            Object value = symbolTable.getValue(_localVariableBeanToListenOn);
171
172            NamedBean namedBean = null;
173
174            if (value instanceof NamedBean) {
175                namedBean = (NamedBean) value;
176            } else if (value != null) {
177                namedBean = _namedBeanType.getManager().getNamedBean(value.toString());
178            }
179
180            if (namedBean != null) {
181                if (!_namedBeansEntries.containsKey(namedBean)) {
182                    _namedBeansEntries.put(namedBean, _namedBeanType.getPropertyName());
183                    addPropertyListener(namedBean, _namedBeanType.getPropertyName());
184                }
185            } else {
186                log.warn("The named bean \"{}\" cannot be found in the manager for {}", value, _namedBeanType.toString());
187            }
188        }
189    }
190
191    @Override
192    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
193        switch (index) {
194            case 0:
195                return _executeSocket;
196
197            default:
198                throw new IllegalArgumentException(
199                        String.format("index has invalid value: %d", index));
200        }
201    }
202
203    @Override
204    public int getChildCount() {
205        return 1;
206    }
207
208    @Override
209    public void connected(FemaleSocket socket) {
210        if (socket == _executeSocket) {
211            _executeSocketSystemName = socket.getConnectedSocket().getSystemName();
212        } else {
213            throw new IllegalArgumentException("unkown socket");
214        }
215    }
216
217    @Override
218    public void disconnected(FemaleSocket socket) {
219        if (socket == _executeSocket) {
220            _executeSocketSystemName = null;
221        } else {
222            throw new IllegalArgumentException("unkown socket");
223        }
224    }
225
226    @Override
227    public String getShortDescription(Locale locale) {
228        return Bundle.getMessage(locale, "ActionListenOnBeansLocalVariable_Short");
229    }
230
231    @Override
232    public String getLongDescription(Locale locale) {
233        return Bundle.getMessage(locale,
234                "ActionListenOnBeansLocalVariable_Long",
235                _localVariableBeanToListenOn,
236                _namedBeanType.toString());
237    }
238
239    /** {@inheritDoc} */
240    @Override
241    public void setup() {
242        try {
243            if (!_executeSocket.isConnected()
244                    || !_executeSocket.getConnectedSocket().getSystemName()
245                            .equals(_executeSocketSystemName)) {
246
247                String socketSystemName = _executeSocketSystemName;
248
249                _executeSocket.disconnect();
250
251                if (socketSystemName != null) {
252                    MaleSocket maleSocket =
253                            InstanceManager.getDefault(DigitalActionManager.class)
254                                    .getBySystemName(socketSystemName);
255                    if (maleSocket != null) {
256                        _executeSocket.connect(maleSocket);
257                        maleSocket.setup();
258                    } else {
259                        log.error("cannot load digital action {}", socketSystemName);
260                    }
261                }
262            } else {
263                _executeSocket.getConnectedSocket().setup();
264            }
265        } catch (SocketAlreadyConnectedException ex) {
266            // This shouldn't happen and is a runtime error if it does.
267            throw new RuntimeException("socket is already connected");
268        }
269    }
270
271    private void addPropertyListener(NamedBean namedBean, String property) {
272        if (!_listenOnAllProperties
273                && (property != null)) {
274            namedBean.addPropertyChangeListener(property, this);
275        } else {
276            namedBean.addPropertyChangeListener(this);
277        }
278    }
279
280    /** {@inheritDoc} */
281    @Override
282    public void registerListenersForThisClass() {
283        if (_listenersAreRegistered) return;
284
285        for (Map.Entry<NamedBean, String> namedBeanEntry : _namedBeansEntries.entrySet()) {
286            addPropertyListener(namedBeanEntry.getKey(), namedBeanEntry.getValue());
287        }
288        _listenersAreRegistered = true;
289    }
290
291    /** {@inheritDoc} */
292    @Override
293    public void unregisterListenersForThisClass() {
294        if (!_listenersAreRegistered) return;
295
296        for (Map.Entry<NamedBean, String> namedBeanEntry : _namedBeansEntries.entrySet()) {
297            if (!_listenOnAllProperties
298                    && (namedBeanEntry.getValue() != null)) {
299                namedBeanEntry.getKey().removePropertyChangeListener(namedBeanEntry.getValue(), this);
300            } else {
301                namedBeanEntry.getKey().removePropertyChangeListener(this);
302            }
303            namedBeanEntry.getKey().removePropertyChangeListener(namedBeanEntry.getValue(), this);
304        }
305        _listenersAreRegistered = false;
306    }
307
308    /** {@inheritDoc} */
309    @Override
310    public void propertyChange(PropertyChangeEvent evt) {
311        boolean isQueueEmpty;
312        synchronized(this) {
313            isQueueEmpty = _eventQueue.isEmpty();
314            _eventQueue.add(evt);
315        }
316        if (isQueueEmpty) {
317            getConditionalNG().execute(_internalSocket);
318        }
319    }
320
321    /** {@inheritDoc} */
322    @Override
323    public void disposeMe() {
324    }
325
326
327    /** {@inheritDoc} */
328    @Override
329    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
330    }
331
332
333    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
334
335        private ConditionalNG _conditionalNG;
336        private SymbolTable _newSymbolTable;
337
338        public InternalFemaleSocket() {
339            super(null, new FemaleSocketListener(){
340                @Override
341                public void connected(FemaleSocket socket) {
342                    // Do nothing
343                }
344
345                @Override
346                public void disconnected(FemaleSocket socket) {
347                    // Do nothing
348                }
349            }, "A");
350        }
351
352        @Override
353        public void execute() throws JmriException {
354            if (_executeSocket != null) {
355
356                synchronized(this) {
357                    SymbolTable oldSymbolTable = _conditionalNG.getSymbolTable();
358                    _conditionalNG.setSymbolTable(_newSymbolTable);
359
360                    String namedBean;
361                    String event;
362                    String newValue;
363
364                    PropertyChangeEvent evt = _eventQueue.poll();
365                    if (evt != null) {
366                        namedBean = ((NamedBean)evt.getSource()).getDisplayName();
367                        event = evt.getPropertyName();
368                        newValue = evt.getNewValue() != null ? evt.getNewValue().toString() : null;
369                    } else {
370                        namedBean = null;
371                        event = null;
372                        newValue = null;
373                    }
374
375                    if (_localVariableNamedBean != null) {
376                        _newSymbolTable.setValue(_localVariableNamedBean, namedBean);
377                    }
378                    if (_localVariableEvent != null) {
379                        _newSymbolTable.setValue(_localVariableEvent, event);
380                    }
381                    if (_localVariableNewValue != null) {
382                        _newSymbolTable.setValue(_localVariableNewValue, newValue);
383                    }
384
385                    _executeSocket.execute();
386                    _conditionalNG.setSymbolTable(oldSymbolTable);
387
388                    if (!_eventQueue.isEmpty()) {
389                        _conditionalNG.execute(_internalSocket);
390                    }
391                }
392            }
393        }
394
395    }
396
397
398    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionListenOnBeansLocalVariable.class);
399
400}