001package jmri.jmrit.logixng.actions;
002
003import jmri.jmrit.logixng.NamedBeanType;
004
005import java.beans.*;
006import java.util.*;
007import java.util.stream.Collectors;
008
009import jmri.*;
010import jmri.jmrit.logixng.*;
011import jmri.jmrit.logixng.util.DuplicateKeyMap;
012
013import net.jcip.annotations.GuardedBy;
014
015/**
016 * This action listens on some beans and runs the ConditionalNG on property change.
017 *
018 * @author Daniel Bergqvist Copyright 2019
019 */
020public class ActionListenOnBeans extends AbstractDigitalAction
021        implements PropertyChangeListener, VetoableChangeListener {
022
023    private final Map<String, NamedBeanReference> _namedBeanReferences = new DuplicateKeyMap<>();
024    private String _localVariableNamedBean;
025    private String _localVariableEvent;
026    private String _localVariableNewValue;
027
028    @GuardedBy("this")
029    private final Deque<PropertyChangeEvent> _eventQueue = new ArrayDeque<>();
030
031
032    public ActionListenOnBeans(String sys, String user)
033            throws BadUserNameException, BadSystemNameException {
034        super(sys, user);
035    }
036
037    @Override
038    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) {
039        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
040        String sysName = systemNames.get(getSystemName());
041        String userName = userNames.get(getSystemName());
042        if (sysName == null) sysName = manager.getAutoSystemName();
043        ActionListenOnBeans copy = new ActionListenOnBeans(sysName, userName);
044        copy.setComment(getComment());
045        copy.setLocalVariableNamedBean(_localVariableNamedBean);
046        copy.setLocalVariableEvent(_localVariableEvent);
047        copy.setLocalVariableNewValue(_localVariableNewValue);
048        for (NamedBeanReference reference : _namedBeanReferences.values()) {
049            copy.addReference(reference);
050        }
051        return manager.registerAction(copy);
052    }
053
054    /**
055     * Register a bean
056     * The bean must be on the form "beantype:name" where beantype is for
057     * example turnout, sensor or memory, and name is the name of the bean.
058     * The type can be upper case or lower case, it doesn't matter.
059     * @param beanAndType the bean and type
060     */
061    public void addReference(String beanAndType) {
062        assertListenersAreNotRegistered(log, "addReference");
063        String[] parts = beanAndType.split(":");
064        if ((parts.length < 2) || (parts.length > 3)) {
065            throw new IllegalArgumentException(
066                    "Parameter 'beanAndType' must be on the format type:name"
067                    + " where type is turnout, sensor, memory, ..., or on the"
068                    + " format type:name:all where all is yes or no");
069        }
070
071        boolean listenToAll = false;
072        if (parts.length == 3) listenToAll = "yes".equals(parts[2]); // NOI18N
073
074        try {
075            NamedBeanType type = NamedBeanType.valueOf(parts[0]);
076            NamedBeanReference reference = new NamedBeanReference(parts[1], type, listenToAll);
077            addReference(reference);
078        } catch (IllegalArgumentException e) {
079            String types = Arrays.asList(NamedBeanType.values())
080                    .stream()
081                    .map(Enum::toString)
082                    .collect(Collectors.joining(", "));
083            throw new IllegalArgumentException(
084                    "Parameter 'beanAndType' has wrong type. Valid types are: " + types);
085        }
086    }
087
088    public void addReference(NamedBeanReference reference) {
089        assertListenersAreNotRegistered(log, "addReference");
090        _namedBeanReferences.put(reference._name, reference);
091        reference._type.getManager().addVetoableChangeListener(this);
092    }
093
094    public void removeReference(NamedBeanReference reference) {
095        assertListenersAreNotRegistered(log, "removeReference");
096        _namedBeanReferences.remove(reference._name, reference);
097        reference._type.getManager().removeVetoableChangeListener(this);
098    }
099
100    public Collection<NamedBeanReference> getReferences() {
101        return _namedBeanReferences.values();
102    }
103
104    public void clearReferences() {
105        _namedBeanReferences.clear();
106    }
107
108    public void setLocalVariableNamedBean(String localVariableNamedBean) {
109        if ((localVariableNamedBean != null) && (!localVariableNamedBean.isEmpty())) {
110            this._localVariableNamedBean = localVariableNamedBean;
111        } else {
112            this._localVariableNamedBean = null;
113        }
114    }
115
116    public String getLocalVariableNamedBean() {
117        return _localVariableNamedBean;
118    }
119
120    public void setLocalVariableEvent(String localVariableEvent) {
121        if ((localVariableEvent != null) && (!localVariableEvent.isEmpty())) {
122            this._localVariableEvent = localVariableEvent;
123        } else {
124            this._localVariableEvent = null;
125        }
126    }
127
128    public String getLocalVariableEvent() {
129        return _localVariableEvent;
130    }
131
132    public void setLocalVariableNewValue(String localVariableNewValue) {
133        if ((localVariableNewValue != null) && (!localVariableNewValue.isEmpty())) {
134            this._localVariableNewValue = localVariableNewValue;
135        } else {
136            this._localVariableNewValue = null;
137        }
138    }
139
140    public String getLocalVariableNewValue() {
141        return _localVariableNewValue;
142    }
143
144    @Override
145    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
146        var tempNamedBeanReferences = new ArrayList<NamedBeanReference>(_namedBeanReferences.values());
147        for (NamedBeanReference reference : tempNamedBeanReferences) {
148            if (reference._type.getClazz().isAssignableFrom(evt.getOldValue().getClass())) {
149                if ((reference._handle != null) && evt.getOldValue().equals(reference._handle.getBean())) {
150                    if ("CanDelete".equals(evt.getPropertyName())) { // No I18N
151                        PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null);
152                        throw new PropertyVetoException(getDisplayName(), e);
153                    } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
154                        _namedBeanReferences.remove(reference._name, reference);
155                    }
156                }
157            }
158        }
159    }
160
161    /** {@inheritDoc} */
162    @Override
163    public Category getCategory() {
164        return Category.OTHER;
165    }
166
167    /** {@inheritDoc} */
168    @Override
169    public void execute() {
170        // The main purpose of this action is only to listen on property
171        // changes of the registered beans and execute the ConditionalNG
172        // when it happens.
173
174        synchronized(this) {
175            String namedBean;
176            String event;
177            String newValue;
178
179            PropertyChangeEvent evt = _eventQueue.poll();
180            if (evt != null) {
181                namedBean = ((NamedBean)evt.getSource()).getDisplayName();
182                event = evt.getPropertyName();
183                newValue = evt.getNewValue() != null ? evt.getNewValue().toString() : null;
184            } else {
185                namedBean = null;
186                event = null;
187                newValue = null;
188            }
189
190            SymbolTable symbolTable = getConditionalNG().getSymbolTable();
191
192            if (_localVariableNamedBean != null) {
193                symbolTable.setValue(_localVariableNamedBean, namedBean);
194            }
195            if (_localVariableEvent != null) {
196                symbolTable.setValue(_localVariableEvent, event);
197            }
198            if (_localVariableNewValue != null) {
199                symbolTable.setValue(_localVariableNewValue, newValue);
200            }
201
202            if (!_eventQueue.isEmpty()) {
203                getConditionalNG().execute();
204            }
205        }
206    }
207
208    @Override
209    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
210        throw new UnsupportedOperationException("Not supported.");
211    }
212
213    @Override
214    public int getChildCount() {
215        return 0;
216    }
217
218    @Override
219    public String getShortDescription(Locale locale) {
220        return Bundle.getMessage(locale, "ActionListenOnBeans_Short");
221    }
222
223    @Override
224    public String getLongDescription(Locale locale) {
225        return Bundle.getMessage(locale, "ActionListenOnBeans_Long");
226    }
227
228    /** {@inheritDoc} */
229    @Override
230    public void setup() {
231        for (NamedBeanReference ref : getReferences()) {
232            if (ref.getType() == NamedBeanType.EntryExit) {
233                // The EntryExit objects were not available during file loading.
234                if (ref.getName() != null && !ref.getName().isEmpty()) {
235                    var nxBean = ref.getType().getManager().getNamedBean(ref.getName());
236                    if (nxBean != null) {
237                        // Change the system name to user name which will also create the named bean handle
238                        ref.setName(nxBean.getUserName());
239                    } else {
240                        log.error("NX bean null for {} during setup", ref.getName());
241                    }
242                } else {
243                    log.error("NX name is null or empty");
244                }
245            }
246        }
247    }
248
249    /** {@inheritDoc} */
250    @Override
251    public void registerListenersForThisClass() {
252        if (_listenersAreRegistered) return;
253
254        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
255            if (namedBeanReference._handle != null) {
256                if (!namedBeanReference._listenOnAllProperties
257                        && (namedBeanReference._type.getPropertyName() != null)) {
258                    namedBeanReference._handle.getBean()
259                            .addPropertyChangeListener(namedBeanReference._type.getPropertyName(), this);
260                } else {
261                    namedBeanReference._handle.getBean()
262                            .addPropertyChangeListener(this);
263                }
264            }
265        }
266        _listenersAreRegistered = true;
267    }
268
269    /** {@inheritDoc} */
270    @Override
271    public void unregisterListenersForThisClass() {
272        if (!_listenersAreRegistered) return;
273
274        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
275            if (namedBeanReference._handle != null) {
276                if (!namedBeanReference._listenOnAllProperties
277                        && (namedBeanReference._type.getPropertyName() != null)) {
278                    namedBeanReference._handle.getBean()
279                            .removePropertyChangeListener(namedBeanReference._type.getPropertyName(), this);
280                } else {
281                    namedBeanReference._handle.getBean()
282                            .removePropertyChangeListener(this);
283                }
284            }
285        }
286        _listenersAreRegistered = false;
287    }
288
289    /** {@inheritDoc} */
290    @Override
291    public void propertyChange(PropertyChangeEvent evt) {
292        boolean isQueueEmpty;
293        synchronized(this) {
294            isQueueEmpty = _eventQueue.isEmpty();
295            _eventQueue.add(evt);
296        }
297        if (isQueueEmpty) {
298            getConditionalNG().execute();
299        }
300    }
301
302    /** {@inheritDoc} */
303    @Override
304    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
305        log.debug("getUsageReport :: ActionListenOnBeans: bean = {}, report = {}", cdl, report);
306        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
307            if (namedBeanReference._handle != null) {
308                if (bean.equals(namedBeanReference._handle.getBean())) {
309                    report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription()));
310                }
311            }
312        }
313    }
314
315    /** {@inheritDoc} */
316    @Override
317    public void disposeMe() {
318    }
319
320
321    public static class NamedBeanReference {
322
323        private String _name;
324        private NamedBeanType _type;
325        private NamedBeanHandle<? extends NamedBean> _handle;
326        private boolean _listenOnAllProperties = false;
327
328        public NamedBeanReference(NamedBeanReference ref) {
329            this(ref._handle, ref._type, ref._listenOnAllProperties);
330        }
331
332        public NamedBeanReference(String name, NamedBeanType type, boolean all) {
333            _name = name;
334            _type = type;
335            _listenOnAllProperties = all;
336
337            if (_type != null) {
338                NamedBean bean = _type.getManager().getNamedBean(name);
339                if (bean != null) {
340                    _handle = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(_name, bean);
341                }
342            }
343        }
344
345        public NamedBeanReference(NamedBeanHandle<? extends NamedBean> handle, NamedBeanType type, boolean all) {
346            _name = handle != null ? handle.getName() : null;
347            _type = type;
348            _listenOnAllProperties = all;
349            _handle = handle;
350        }
351
352        public String getName() {
353            return _name;
354        }
355
356        public void setName(String name) {
357            _name = name;
358            updateHandle();
359        }
360
361        public void setName(NamedBean bean) {
362            if (bean != null) {
363                _handle = InstanceManager.getDefault(NamedBeanHandleManager.class)
364                        .getNamedBeanHandle(bean.getDisplayName(), bean);
365                _name = _handle.getName();
366            } else {
367                _name = null;
368                _handle = null;
369            }
370        }
371
372        public void setName(NamedBeanHandle<? extends NamedBean> handle) {
373            if (handle != null) {
374                _handle = handle;
375                _name = _handle.getName();
376            } else {
377                _name = null;
378                _handle = null;
379            }
380        }
381
382        public NamedBeanType getType() {
383            return _type;
384        }
385
386        public void setType(NamedBeanType type) {
387            if (type == null) {
388                log.warn("type is null");
389                type = NamedBeanType.Turnout;
390            }
391            _type = type;
392            _handle = null;
393        }
394
395        public NamedBeanHandle<? extends NamedBean> getHandle() {
396            return _handle;
397        }
398
399        private void updateHandle() {
400            if (_type != null && _name != null && !_name.isEmpty()) {
401                NamedBean bean = _type.getManager().getNamedBean(_name);
402                if (bean != null) {
403                    _handle = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(_name, bean);
404                } else {
405                    log.warn("Cannot find named bean {} in manager for {}", _name, _type.getManager().getBeanTypeHandled());
406                    _handle = null;
407                }
408            } else {
409                _handle = null;
410            }
411        }
412
413        public boolean getListenOnAllProperties() {
414            return _listenOnAllProperties;
415        }
416
417        public void setListenOnAllProperties(boolean listenOnAllProperties) {
418            _listenOnAllProperties = listenOnAllProperties;
419        }
420
421        // This method is used by ListenOnBeansTableModel
422        @Override
423        public String toString() {
424            if (_handle != null) return _handle.getName();
425            else return "";
426        }
427    }
428
429    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionListenOnBeans.class);
430
431}