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        // Do nothing
232    }
233
234    /** {@inheritDoc} */
235    @Override
236    public void registerListenersForThisClass() {
237        if (_listenersAreRegistered) return;
238
239        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
240            if (namedBeanReference._handle != null) {
241                if (!namedBeanReference._listenOnAllProperties
242                        && (namedBeanReference._type.getPropertyName() != null)) {
243                    namedBeanReference._handle.getBean()
244                            .addPropertyChangeListener(namedBeanReference._type.getPropertyName(), this);
245                } else {
246                    namedBeanReference._handle.getBean()
247                            .addPropertyChangeListener(this);
248                }
249            }
250        }
251        _listenersAreRegistered = true;
252    }
253
254    /** {@inheritDoc} */
255    @Override
256    public void unregisterListenersForThisClass() {
257        if (!_listenersAreRegistered) return;
258
259        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
260            if (namedBeanReference._handle != null) {
261                if (!namedBeanReference._listenOnAllProperties
262                        && (namedBeanReference._type.getPropertyName() != null)) {
263                    namedBeanReference._handle.getBean()
264                            .removePropertyChangeListener(namedBeanReference._type.getPropertyName(), this);
265                } else {
266                    namedBeanReference._handle.getBean()
267                            .removePropertyChangeListener(this);
268                }
269            }
270        }
271        _listenersAreRegistered = false;
272    }
273
274    /** {@inheritDoc} */
275    @Override
276    public void propertyChange(PropertyChangeEvent evt) {
277        boolean isQueueEmpty;
278        synchronized(this) {
279            isQueueEmpty = _eventQueue.isEmpty();
280            _eventQueue.add(evt);
281        }
282        if (isQueueEmpty) {
283            getConditionalNG().execute();
284        }
285    }
286
287    /** {@inheritDoc} */
288    @Override
289    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
290        log.debug("getUsageReport :: ActionListenOnBeans: bean = {}, report = {}", cdl, report);
291        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
292            if (namedBeanReference._handle != null) {
293                if (bean.equals(namedBeanReference._handle.getBean())) {
294                    report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription()));
295                }
296            }
297        }
298    }
299
300    /** {@inheritDoc} */
301    @Override
302    public void disposeMe() {
303    }
304
305
306    public static class NamedBeanReference {
307
308        private String _name;
309        private NamedBeanType _type;
310        private NamedBeanHandle<? extends NamedBean> _handle;
311        private boolean _listenOnAllProperties = false;
312
313        public NamedBeanReference(NamedBeanReference ref) {
314            this(ref._handle, ref._type, ref._listenOnAllProperties);
315        }
316
317        public NamedBeanReference(String name, NamedBeanType type, boolean all) {
318            _name = name;
319            _type = type;
320            _listenOnAllProperties = all;
321
322            if (_type != null) {
323                NamedBean bean = _type.getManager().getNamedBean(name);
324                if (bean != null) {
325                    _handle = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(_name, bean);
326                }
327            }
328        }
329
330        public NamedBeanReference(NamedBeanHandle<? extends NamedBean> handle, NamedBeanType type, boolean all) {
331            _name = handle != null ? handle.getName() : null;
332            _type = type;
333            _listenOnAllProperties = all;
334            _handle = handle;
335        }
336
337        public String getName() {
338            return _name;
339        }
340
341        public void setName(String name) {
342            _name = name;
343            updateHandle();
344        }
345
346        public void setName(NamedBean bean) {
347            if (bean != null) {
348                _handle = InstanceManager.getDefault(NamedBeanHandleManager.class)
349                        .getNamedBeanHandle(bean.getDisplayName(), bean);
350                _name = _handle.getName();
351            } else {
352                _name = null;
353                _handle = null;
354            }
355        }
356
357        public void setName(NamedBeanHandle<? extends NamedBean> handle) {
358            if (handle != null) {
359                _handle = handle;
360                _name = _handle.getName();
361            } else {
362                _name = null;
363                _handle = null;
364            }
365        }
366
367        public NamedBeanType getType() {
368            return _type;
369        }
370
371        public void setType(NamedBeanType type) {
372            if (type == null) {
373                log.warn("type is null");
374                type = NamedBeanType.Turnout;
375            }
376            _type = type;
377            _handle = null;
378        }
379
380        public NamedBeanHandle<? extends NamedBean> getHandle() {
381            return _handle;
382        }
383
384        private void updateHandle() {
385            if (_type != null && _name != null && !_name.isEmpty()) {
386                NamedBean bean = _type.getManager().getNamedBean(_name);
387                if (bean != null) {
388                    _handle = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(_name, bean);
389                } else {
390                    log.warn("Cannot find named bean {} in manager for {}", _name, _type.getManager().getBeanTypeHandled());
391                    _handle = null;
392                }
393            } else {
394                _handle = null;
395            }
396        }
397
398        public boolean getListenOnAllProperties() {
399            return _listenOnAllProperties;
400        }
401
402        public void setListenOnAllProperties(boolean listenOnAllProperties) {
403            _listenOnAllProperties = listenOnAllProperties;
404        }
405
406        // This method is used by ListenOnBeansTableModel
407        @Override
408        public String toString() {
409            if (_handle != null) return _handle.getName();
410            else return "";
411        }
412    }
413
414    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionListenOnBeans.class);
415
416}