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