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