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