001package jmri.jmrit.logixng.util;
002
003import java.beans.*;
004import java.util.*;
005
006import javax.annotation.CheckForNull;
007import javax.annotation.Nonnull;
008
009import jmri.*;
010import jmri.jmrit.logixng.*;
011import jmri.jmrit.logixng.implementation.AbstractBase;
012import jmri.jmrit.logixng.util.parser.*;
013import jmri.jmrit.logixng.util.parser.RecursiveDescentParser;
014import jmri.util.TypeConversionUtil;
015
016/**
017 * Select namedBean for LogixNG actions and expressions.
018 *
019 * @param <E> the type of the named bean
020 *
021 * @author Daniel Bergqvist (C) 2022
022 */
023public class LogixNG_SelectNamedBean<E extends NamedBean> implements VetoableChangeListener {
024
025    private final AbstractBase _base;
026    private final InUse _inUse;
027    private final Class<E> _class;
028    private final Manager<E> _manager;
029    private final LogixNG_SelectTable _selectTable;
030    private final PropertyChangeListener _listener;
031    private boolean _listenToMemory;
032    private boolean _listenersAreRegistered;
033    private boolean _onlyDirectAddressingAllowed;
034
035    private NamedBeanAddressing _addressing = NamedBeanAddressing.Direct;
036    private NamedBeanHandle<E> _handle;
037    private String _reference = "";
038    private NamedBeanHandle<Memory> _memoryHandle;
039    private String _localVariable = "";
040    private String _formula = "";
041    private ExpressionNode _expressionNode;
042
043    private String _delayedNamedBean;
044
045
046    public LogixNG_SelectNamedBean(AbstractBase base, Class<E> clazz, Manager<E> manager, PropertyChangeListener listener) {
047        _base = base;
048        _inUse = () -> true;
049        _class = clazz;
050        _manager = manager;
051        _selectTable = new LogixNG_SelectTable(_base, _inUse);
052        _listener = listener;
053    }
054
055    public LogixNG_SelectNamedBean(AbstractBase base, Class<E> clazz, Manager<E> manager, InUse inUse, PropertyChangeListener listener) {
056        _base = base;
057        _inUse = inUse;
058        _class = clazz;
059        _manager = manager;
060        _selectTable = new LogixNG_SelectTable(_base, _inUse);
061        _listener = listener;
062    }
063
064    public void setOnlyDirectAddressingAllowed() {
065        _onlyDirectAddressingAllowed = true;
066    }
067
068    public boolean getOnlyDirectAddressingAllowed() {
069        return _onlyDirectAddressingAllowed;
070    }
071
072    public void copy(LogixNG_SelectNamedBean<E> copy) throws ParserException {
073        copy.setAddressing(_addressing);
074        if (_handle != null) copy.setNamedBean(_handle);
075        copy.setLocalVariable(_localVariable);
076        copy.setReference(_reference);
077        copy.setMemory(_memoryHandle);
078        copy.setListenToMemory(_listenToMemory);
079        copy.setFormula(_formula);
080        _selectTable.copy(copy._selectTable);
081    }
082
083    public Manager<E> getManager() {
084        return _manager;
085    }
086
087    public void setAddressing(@Nonnull NamedBeanAddressing addressing) throws ParserException {
088        if (_onlyDirectAddressingAllowed && (addressing != NamedBeanAddressing.Direct)) {
089            throw new IllegalArgumentException("Addressing must be Direct");
090        }
091        this._addressing = addressing;
092        parseFormula();
093    }
094
095    public boolean isDirectAddressing() {
096        return _addressing == NamedBeanAddressing.Direct;
097    }
098
099    public NamedBeanAddressing getAddressing() {
100        return _addressing;
101    }
102
103    public void setDelayedNamedBean(@Nonnull String name) {
104        _delayedNamedBean = name;
105    }
106
107    public void setup() {
108        if (_delayedNamedBean != null) setNamedBean(_delayedNamedBean);
109    }
110
111    public void setNamedBean(@Nonnull String name) {
112        _base.assertListenersAreNotRegistered(log, "setNamedBean");
113        E namedBean = _manager.getNamedBean(name);
114        if (namedBean != null) {
115            setNamedBean(name, namedBean);
116        } else {
117            removeNamedBean();
118            log.error("{} \"{}\" is not found", _manager.getBeanTypeHandled(), name);
119        }
120    }
121
122    public void setNamedBean(@Nonnull NamedBeanHandle<E> handle) {
123        _base.assertListenersAreNotRegistered(log, "setNamedBean");
124        _handle = handle;
125        _manager.addVetoableChangeListener(this);
126    }
127
128    public void setNamedBean(@Nonnull E namedBean) {
129        setNamedBean(namedBean.getDisplayName(), namedBean);
130    }
131
132    public void setNamedBean(@Nonnull String name, @Nonnull E namedBean) {
133        _base.assertListenersAreNotRegistered(log, "setNamedBean");
134        setNamedBean(InstanceManager.getDefault(NamedBeanHandleManager.class)
135                .getNamedBeanHandle(name, namedBean));
136    }
137
138    public void removeNamedBean() {
139        _base.assertListenersAreNotRegistered(log, "setNamedBean");
140        if (_handle != null) {
141            _manager.removeVetoableChangeListener(this);
142            _handle = null;
143        }
144    }
145
146    public E getBean() {
147        if (_handle != null) {
148            return _handle.getBean();
149        } else {
150            return null;
151        }
152    }
153
154    public NamedBeanHandle<E> getNamedBean() {
155        return _handle;
156    }
157
158    public E getNamedBeanIfDirectAddressing() {
159        if ((_handle != null) && (this._addressing == NamedBeanAddressing.Direct)) {
160            return _handle.getBean();
161        }
162        return null;
163    }
164
165    public void setReference(@Nonnull String reference) {
166        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
167            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
168        }
169        _reference = reference;
170    }
171
172    public String getReference() {
173        return _reference;
174    }
175
176    public void setMemory(@Nonnull String memoryName) {
177        Memory memory = InstanceManager.getDefault(MemoryManager.class).getMemory(memoryName);
178        if (memory != null) {
179            setMemory(memory);
180        } else {
181            removeMemory();
182            log.warn("memory \"{}\" is not found", memoryName);
183        }
184    }
185
186    public void setMemory(@Nonnull NamedBeanHandle<Memory> handle) {
187        _memoryHandle = handle;
188        InstanceManager.memoryManagerInstance().addVetoableChangeListener(this);
189        addRemoveVetoListener();
190    }
191
192    public void setMemory(@Nonnull Memory memory) {
193        setMemory(InstanceManager.getDefault(NamedBeanHandleManager.class)
194                .getNamedBeanHandle(memory.getDisplayName(), memory));
195    }
196
197    public void removeMemory() {
198        if (_memoryHandle != null) {
199            _memoryHandle = null;
200            addRemoveVetoListener();
201        }
202    }
203
204    public NamedBeanHandle<Memory> getMemory() {
205        return _memoryHandle;
206    }
207
208    public void setListenToMemory(boolean listenToMemory) {
209        _listenToMemory = listenToMemory;
210    }
211
212    public boolean getListenToMemory() {
213        return _listenToMemory;
214    }
215
216    public void setLocalVariable(@Nonnull String localVariable) {
217        _localVariable = localVariable;
218    }
219
220    public String getLocalVariable() {
221        return _localVariable;
222    }
223
224    public void setFormula(@Nonnull String formula) throws ParserException {
225        _formula = formula;
226        parseFormula();
227    }
228
229    public String getFormula() {
230        return _formula;
231    }
232
233    private void parseFormula() throws ParserException {
234        if (_addressing == NamedBeanAddressing.Formula) {
235            Map<String, Variable> variables = new HashMap<>();
236
237            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
238            _expressionNode = parser.parseExpression(_formula);
239        } else {
240            _expressionNode = null;
241        }
242    }
243
244    public LogixNG_SelectTable getSelectTable() {
245        return _selectTable;
246    }
247
248    private void addRemoveVetoListener() {
249        if (_memoryHandle != null) {
250            InstanceManager.getDefault(MemoryManager.class).addVetoableChangeListener(this);
251        } else {
252            InstanceManager.getDefault(MemoryManager.class).removeVetoableChangeListener(this);
253        }
254    }
255
256    public E evaluateNamedBean(ConditionalNG conditionalNG) throws JmriException {
257
258        if (_addressing == NamedBeanAddressing.Direct) {
259            return _handle != null ? _handle.getBean() : null;
260        } else {
261            String name;
262
263            switch (_addressing) {
264                case Reference:
265                    name = ReferenceUtil.getReference(
266                            conditionalNG.getSymbolTable(), _reference);
267                    break;
268
269                case LocalVariable:
270                    SymbolTable symbolNamedBean = conditionalNG.getSymbolTable();
271                    name = TypeConversionUtil
272                            .convertToString(symbolNamedBean.getValue(_localVariable), false);
273                    break;
274
275                case Memory:
276                    name = TypeConversionUtil
277                            .convertToString(_memoryHandle.getBean().getValue(), false);
278                    break;
279
280                case Formula:
281                    name = _expressionNode  != null
282                            ? TypeConversionUtil.convertToString(
283                                    _expressionNode.calculate(
284                                            conditionalNG.getSymbolTable()), false)
285                            : null;
286                    break;
287
288                case Table:
289                    name = TypeConversionUtil.convertToString(
290                            _selectTable.evaluateTableData(conditionalNG), false);
291                    break;
292
293                default:
294                    throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name());
295            }
296
297            E namedBean = null;
298            if (name != null) {
299                namedBean = _manager.getNamedBean(name);
300            }
301            return namedBean;
302        }
303    }
304
305    public String getDescription(Locale locale) {
306        String namedBean;
307
308        String memoryName;
309        if (_memoryHandle != null) {
310            memoryName = _memoryHandle.getName();
311        } else {
312            memoryName = Bundle.getMessage(locale, "BeanNotSelected");
313        }
314
315        switch (_addressing) {
316            case Direct:
317                String namedBeanName;
318                if (_handle != null) {
319                    namedBeanName = _handle.getBean().getDisplayName();
320                } else {
321                    namedBeanName = Bundle.getMessage(locale, "BeanNotSelected");
322                }
323                namedBean = Bundle.getMessage(locale, "AddressByDirect", namedBeanName);
324                break;
325
326            case Reference:
327                namedBean = Bundle.getMessage(locale, "AddressByReference", _reference);
328                break;
329
330            case Memory:
331                namedBean = Bundle.getMessage(locale, "AddressByMemory_Listen", memoryName, Base.getListenString(_listenToMemory));
332                break;
333
334            case LocalVariable:
335                namedBean = Bundle.getMessage(locale, "AddressByLocalVariable", _localVariable);
336                break;
337
338            case Formula:
339                namedBean = Bundle.getMessage(locale, "AddressByFormula", _formula);
340                break;
341
342            case Table:
343                namedBean = Bundle.getMessage(
344                        locale,
345                        "AddressByTable",
346                        _selectTable.getTableNameDescription(locale),
347                        _selectTable.getTableRowDescription(locale),
348                        _selectTable.getTableColumnDescription(locale));
349                break;
350
351            default:
352                throw new IllegalArgumentException("invalid _addressing: " + _addressing.name());
353        }
354        return namedBean;
355    }
356
357    /**
358     * Register listeners if this object needs that.
359     */
360    public void registerListeners() {
361        if (!_listenersAreRegistered
362                && (_addressing == NamedBeanAddressing.Memory)
363                && (_memoryHandle != null)
364                && _listenToMemory) {
365            _memoryHandle.getBean().addPropertyChangeListener("value", _listener);
366            _listenersAreRegistered = true;
367        }
368    }
369
370    /**
371     * Unregister listeners if this object needs that.
372     */
373    public void unregisterListeners() {
374        if (_listenersAreRegistered
375                && (_addressing == NamedBeanAddressing.Memory)
376                && (_memoryHandle != null)
377                && _listenToMemory) {
378            _memoryHandle.getBean().removePropertyChangeListener("value", _listener);
379            _listenersAreRegistered = false;
380        }
381    }
382
383    @Override
384    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
385        if ("CanDelete".equals(evt.getPropertyName()) && _inUse.isInUse()) { // No I18N
386            if (_inUse.isInUse() && (_class.isAssignableFrom(evt.getOldValue().getClass()))) {
387                if (evt.getOldValue().equals(getNamedBean().getBean())) {
388                    PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null);
389                    throw new PropertyVetoException(Bundle.getMessage("InUseVeto", _base.getDisplayName(), _base.getShortDescription()), e);
390                }
391            }
392            if (evt.getOldValue() instanceof Memory) {
393                boolean doVeto = false;
394                if ((_addressing == NamedBeanAddressing.Memory) && (_memoryHandle != null) && evt.getOldValue().equals(_memoryHandle.getBean())) {
395                    doVeto = true;
396                }
397                if (doVeto) {
398                    PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null);
399                    throw new PropertyVetoException(Bundle.getMessage("MemoryInUseMemoryExpressionVeto", _base.getDisplayName()), e); // NOI18N
400                }
401            }
402        } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
403            if (_class.isAssignableFrom(evt.getOldValue().getClass())) {
404                if (evt.getOldValue().equals(getNamedBean().getBean())) {
405                    removeNamedBean();
406                }
407            }
408            if (evt.getOldValue() instanceof Memory) {
409                if ((_memoryHandle != null) && evt.getOldValue().equals(_memoryHandle.getBean())) {
410                    removeMemory();
411                }
412            }
413        }
414    }
415
416    /**
417     * Add a {@link java.beans.PropertyChangeListener} for a specific property.
418     *
419     * @param listener The PropertyChangeListener to be added
420     */
421    public void addPropertyChangeListener(
422            @CheckForNull PropertyChangeListener listener) {
423        if ((_addressing == NamedBeanAddressing.Direct) && (_handle != null)) {
424            _handle.getBean().addPropertyChangeListener(listener);
425        }
426    }
427
428    /**
429     * Add a {@link java.beans.PropertyChangeListener} for a specific property.
430     *
431     * @param propertyName The name of the property to listen on.
432     * @param listener     The PropertyChangeListener to be added
433     */
434    public void addPropertyChangeListener(
435            @CheckForNull String propertyName,
436            @CheckForNull PropertyChangeListener listener) {
437        if ((_addressing == NamedBeanAddressing.Direct) && (_handle != null)) {
438            _handle.getBean().addPropertyChangeListener(propertyName, listener);
439        }
440    }
441
442    /**
443     * Remove the specified listener of the specified property from this object.
444     *
445     * @param listener The {@link java.beans.PropertyChangeListener} to
446     *                 remove.
447     */
448    public void removePropertyChangeListener(
449            @CheckForNull PropertyChangeListener listener) {
450        if (_handle != null) {
451            _handle.getBean().removePropertyChangeListener(listener);
452        }
453    }
454
455    /**
456     * Remove the specified listener of the specified property from this object.
457     *
458     * @param propertyName The name of the property to stop listening to.
459     * @param listener     The {@link java.beans.PropertyChangeListener} to
460     *                     remove.
461     */
462    public void removePropertyChangeListener(
463            @CheckForNull String propertyName,
464            @CheckForNull PropertyChangeListener listener) {
465        if (_handle != null) {
466            _handle.getBean().removePropertyChangeListener(propertyName, listener);
467        }
468    }
469
470    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl, Base base, Type type) {
471        log.debug("getUsageReport :: {}: bean = {}, report = {}", base.getShortDescription(), cdl, report);
472        if (_handle != null && bean.equals(_handle.getBean())) {
473            report.add(new NamedBeanUsageReport(type.toString(), cdl, base.getLongDescription()));
474        }
475    }
476
477    public static enum Type {
478        Action("LogixNGAction"),
479        Expression("LogixNGExpression");
480
481        private final String _descr;
482
483        private Type(String descr) {
484            this._descr = descr;
485        }
486
487        @Override
488        public String toString() {
489            return _descr;
490        }
491    }
492
493    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogixNG_SelectNamedBean.class);
494}