001package jmri.jmrit.logixng.actions;
002
003import jmri.jmrit.logixng.NamedBeanType;
004
005import java.beans.*;
006import java.util.*;
007
008import javax.annotation.Nonnull;
009
010import jmri.*;
011import jmri.jmrit.logixng.*;
012import jmri.jmrit.logixng.util.LogixNG_SelectNamedBean;
013import jmri.jmrit.logixng.util.parser.ParserException;
014
015import net.jcip.annotations.GuardedBy;
016
017/**
018 * This action listens on some beans and runs the ConditionalNG on property change.
019 *
020 * @author Daniel Bergqvist Copyright 2019
021 */
022public class ActionListenOnBeansTable extends AbstractDigitalAction
023        implements PropertyChangeListener, VetoableChangeListener {
024
025    private NamedBeanType _namedBeanType = NamedBeanType.Light;
026    private final LogixNG_SelectNamedBean<NamedTable> _selectNamedBean =
027            new LogixNG_SelectNamedBean<>(
028                    this, NamedTable.class, InstanceManager.getDefault(NamedTableManager.class), this);
029    private TableRowOrColumn _tableRowOrColumn = TableRowOrColumn.Row;
030    private String _rowOrColumnName = "";
031    private boolean _includeCellsWithoutHeader = false;
032    private boolean _listenOnAllProperties = false;
033    private final List<Map.Entry<NamedBean, String>> _namedBeansEntries = new ArrayList<>();
034    private String _localVariableNamedBean;
035    private String _localVariableEvent;
036    private String _localVariableNewValue;
037
038    @GuardedBy("this")
039    private final Deque<PropertyChangeEvent> _eventQueue = new ArrayDeque<>();
040
041    public ActionListenOnBeansTable(String sys, String user)
042            throws BadUserNameException, BadSystemNameException {
043        super(sys, user);
044        _selectNamedBean.setOnlyDirectAddressingAllowed();
045    }
046
047    @Override
048    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws ParserException {
049        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
050        String sysName = systemNames.get(getSystemName());
051        String userName = userNames.get(getSystemName());
052        if (sysName == null) sysName = manager.getAutoSystemName();
053        ActionListenOnBeansTable copy = new ActionListenOnBeansTable(sysName, userName);
054        copy.setComment(getComment());
055        copy.setNamedBeanType(_namedBeanType);
056        _selectNamedBean.copy(copy._selectNamedBean);
057        copy.setTableRowOrColumn(_tableRowOrColumn);
058        copy.setRowOrColumnName(_rowOrColumnName);
059        copy.setIncludeCellsWithoutHeader(_includeCellsWithoutHeader);
060
061        copy.setLocalVariableNamedBean(_localVariableNamedBean);
062        copy.setLocalVariableEvent(_localVariableEvent);
063        copy.setLocalVariableNewValue(_localVariableNewValue);
064
065        for (var entry : _namedBeansEntries) {
066            copy._namedBeansEntries.add(
067                    new HashMap.SimpleEntry<>(entry.getKey(), entry.getValue()));
068        }
069
070        return manager.registerAction(copy);
071    }
072
073    /**
074     * Get the type of the named beans
075     * @return the type of named beans
076     */
077    public NamedBeanType getNamedBeanType() {
078        return _namedBeanType;
079    }
080
081    /**
082     * Set the type of the named beans
083     * @param namedBeanType the type of the named beans
084     */
085    public void setNamedBeanType(@Nonnull NamedBeanType namedBeanType) {
086        if (namedBeanType == null) throw new RuntimeException("Daniel");
087        _namedBeanType = namedBeanType;
088    }
089
090    public LogixNG_SelectNamedBean<NamedTable> getSelectNamedBean() {
091        return _selectNamedBean;
092    }
093
094    /**
095     * Get tableRowOrColumn.
096     * @return tableRowOrColumn
097     */
098    public TableRowOrColumn getTableRowOrColumn() {
099        return _tableRowOrColumn;
100    }
101
102    /**
103     * Set tableRowOrColumn.
104     * @param tableRowOrColumn tableRowOrColumn
105     */
106    public void setTableRowOrColumn(@Nonnull TableRowOrColumn tableRowOrColumn) {
107        _tableRowOrColumn = tableRowOrColumn;
108    }
109
110    /**
111     * Get name of row or column
112     * @return name of row or column
113     */
114    public String getRowOrColumnName() {
115        return _rowOrColumnName;
116    }
117
118    /**
119     * Set name of row or column
120     * @param rowOrColumnName name of row or column
121     */
122    public void setRowOrColumnName(@Nonnull String rowOrColumnName) {
123        if (rowOrColumnName == null) throw new IllegalArgumentException("Row/column name is null");
124        _rowOrColumnName = rowOrColumnName;
125    }
126
127    public boolean getListenOnAllProperties() {
128        return _listenOnAllProperties;
129    }
130
131    public void setListenOnAllProperties(boolean listenOnAllProperties) {
132        _listenOnAllProperties = listenOnAllProperties;
133    }
134
135    /**
136     * Set whenever to include cells that doesn't have a header.
137     * Cells without headers can be used to use some cells in the table
138     * as comments.
139     * @return true if include cells that doesn't have a header, false otherwise
140     */
141    public boolean getIncludeCellsWithoutHeader() {
142        return _includeCellsWithoutHeader;
143    }
144
145    /**
146     * Set whenever to include cells that doesn't have a header.
147     * Cells without headers can be used to use some cells in the table
148     * as comments.
149     * @param includeCellsWithoutHeader true if include rows/columns that
150     *                                  doesn't have a header, false otherwise
151     */
152    public void setIncludeCellsWithoutHeader(boolean includeCellsWithoutHeader) {
153        _includeCellsWithoutHeader = includeCellsWithoutHeader;
154    }
155
156    public void setLocalVariableNamedBean(String localVariableNamedBean) {
157        if ((localVariableNamedBean != null) && (!localVariableNamedBean.isEmpty())) {
158            this._localVariableNamedBean = localVariableNamedBean;
159        } else {
160            this._localVariableNamedBean = null;
161        }
162    }
163
164    public String getLocalVariableNamedBean() {
165        return _localVariableNamedBean;
166    }
167
168    public void setLocalVariableEvent(String localVariableEvent) {
169        if ((localVariableEvent != null) && (!localVariableEvent.isEmpty())) {
170            this._localVariableEvent = localVariableEvent;
171        } else {
172            this._localVariableEvent = null;
173        }
174    }
175
176    public String getLocalVariableEvent() {
177        return _localVariableEvent;
178    }
179
180    public void setLocalVariableNewValue(String localVariableNewValue) {
181        if ((localVariableNewValue != null) && (!localVariableNewValue.isEmpty())) {
182            this._localVariableNewValue = localVariableNewValue;
183        } else {
184            this._localVariableNewValue = null;
185        }
186    }
187
188    public String getLocalVariableNewValue() {
189        return _localVariableNewValue;
190    }
191
192    /** {@inheritDoc} */
193    @Override
194    public Category getCategory() {
195        return Category.OTHER;
196    }
197
198    /** {@inheritDoc} */
199    @Override
200    public void execute() {
201        // The purpose of this action is only to listen on property changes
202        // of the registered beans and execute the ConditionalNG when it
203        // happens.
204
205        String namedBean;
206        String event;
207        String newValue;
208
209        synchronized(this) {
210            PropertyChangeEvent evt = _eventQueue.poll();
211            if (evt != null) {
212                namedBean = ((NamedBean)evt.getSource()).getDisplayName();
213                event = evt.getPropertyName();
214                newValue = evt.getNewValue() != null ? evt.getNewValue().toString() : null;
215            } else {
216                namedBean = null;
217                event = null;
218                newValue = null;
219            }
220
221            SymbolTable symbolTable = getConditionalNG().getSymbolTable();
222
223            if (_localVariableNamedBean != null) {
224                symbolTable.setValue(_localVariableNamedBean, namedBean);
225            }
226            if (_localVariableEvent != null) {
227                symbolTable.setValue(_localVariableEvent, event);
228            }
229            if (_localVariableNewValue != null) {
230                symbolTable.setValue(_localVariableNewValue, newValue);
231            }
232
233            if (!_eventQueue.isEmpty()) {
234                getConditionalNG().execute();
235            }
236        }
237    }
238
239    @Override
240    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
241        throw new UnsupportedOperationException("Not supported.");
242    }
243
244    @Override
245    public int getChildCount() {
246        return 0;
247    }
248
249    @Override
250    public String getShortDescription(Locale locale) {
251        return Bundle.getMessage(locale, "ActionListenOnBeansTable_Short");
252    }
253
254    @Override
255    public String getLongDescription(Locale locale) {
256        String tableName = _selectNamedBean.getDescription(locale);
257        return Bundle.getMessage(locale, "ActionListenOnBeansTable_Long",
258                _namedBeanType.toString(),
259                _tableRowOrColumn.getOpposite().toStringLowerCase(),
260                _tableRowOrColumn.toStringLowerCase(),
261                _rowOrColumnName,
262                tableName);
263    }
264
265    /** {@inheritDoc} */
266    @Override
267    public void setup() {
268        // Do nothing
269    }
270
271    public List<String> getItems() {
272        List<String> items = new ArrayList<>();
273
274        if (_selectNamedBean.getNamedBean() == null) {
275            log.error("No table name is given");
276            return items;   // The list is empty
277        }
278        if (_rowOrColumnName.isEmpty()) {
279            log.error("rowOrColumnName is empty string");
280            return items;   // The list is empty
281        }
282
283        NamedTable table = _selectNamedBean.getNamedBean().getBean();
284
285        if (_tableRowOrColumn == TableRowOrColumn.Row) {
286            int row = table.getRowNumber(_rowOrColumnName);
287            for (int column=1; column <= table.numColumns(); column++) {
288                // If the header is null or empty, treat the row as a comment
289                // unless _includeRowColumnWithoutHeader is true
290                Object header = table.getCell(0, column);
291//                System.out.format("Row header: %s%n", header);
292                if (_includeCellsWithoutHeader
293                        || ((header != null) && (!header.toString().isEmpty()))) {
294                    Object cell = table.getCell(row, column);
295                    if (cell != null) items.add(cell.toString());
296                }
297            }
298        } else {
299            int column = table.getColumnNumber(_rowOrColumnName);
300            for (int row=1; row <= table.numRows(); row++) {
301                // If the header is null or empty, treat the row as a comment
302                // unless _includeRowColumnWithoutHeader is true
303                Object header = table.getCell(row, 0);
304//                System.out.format("Column header: %s%n", header);
305                if (_includeCellsWithoutHeader
306                        || ((header != null) && (!header.toString().isEmpty()))) {
307                    Object cell = table.getCell(row, column);
308                    if (cell != null && !cell.toString().isEmpty()) items.add(cell.toString());
309                }
310            }
311        }
312        return items;
313    }
314
315    /** {@inheritDoc} */
316    @Override
317    public void registerListenersForThisClass() {
318        if (_listenersAreRegistered) return;
319
320        List<String> items = getItems();
321
322        for (String item : items) {
323            NamedBean namedBean = _namedBeanType.getManager().getNamedBean(item);
324
325            if (namedBean != null) {
326                Map.Entry<NamedBean, String> namedBeanEntry =
327                        new HashMap.SimpleEntry<>(namedBean, _namedBeanType.getPropertyName());
328
329                _namedBeansEntries.add(namedBeanEntry);
330                if (!_listenOnAllProperties
331                        && (_namedBeanType.getPropertyName() != null)) {
332                    namedBean.addPropertyChangeListener(_namedBeanType.getPropertyName(), this);
333                } else {
334                    namedBean.addPropertyChangeListener(this);
335                }
336            } else {
337                log.warn("The named bean \"{}\" cannot be found in the manager for {}", item, _namedBeanType.toString());
338            }
339        }
340        _selectNamedBean.registerListeners();
341        _listenersAreRegistered = true;
342    }
343
344    /** {@inheritDoc} */
345    @Override
346    public void unregisterListenersForThisClass() {
347        if (!_listenersAreRegistered) return;
348
349        for (Map.Entry<NamedBean, String> namedBeanEntry : _namedBeansEntries) {
350            if (!_listenOnAllProperties
351                    && (namedBeanEntry.getValue() != null)) {
352                namedBeanEntry.getKey().removePropertyChangeListener(namedBeanEntry.getValue(), this);
353            } else {
354                namedBeanEntry.getKey().removePropertyChangeListener(this);
355            }
356            namedBeanEntry.getKey().removePropertyChangeListener(namedBeanEntry.getValue(), this);
357        }
358        _selectNamedBean.unregisterListeners();
359        _listenersAreRegistered = false;
360    }
361
362    /** {@inheritDoc} */
363    @Override
364    public void propertyChange(PropertyChangeEvent evt) {
365        boolean isQueueEmpty;
366        synchronized(this) {
367            isQueueEmpty = _eventQueue.isEmpty();
368            _eventQueue.add(evt);
369        }
370        if (isQueueEmpty) {
371            getConditionalNG().execute();
372        }
373    }
374
375    /** {@inheritDoc} */
376    @Override
377    public void disposeMe() {
378    }
379
380
381    /** {@inheritDoc} */
382    @Override
383    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
384/*
385        log.debug("getUsageReport :: ActionListenOnBeans: bean = {}, report = {}", cdl, report);
386        for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) {
387            if (namedBeanReference._handle != null) {
388                if (bean.equals(namedBeanReference._handle.getBean())) {
389                    report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription()));
390                }
391            }
392        }
393*/
394    }
395
396    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionListenOnBeansTable.class);
397
398}