001package jmri.jmrit.logixng.actions;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006
007import javax.annotation.Nonnull;
008
009import jmri.*;
010import jmri.jmrit.logixng.*;
011import jmri.jmrit.logixng.util.LogixNG_SelectNamedBean;
012import jmri.jmrit.logixng.util.ReferenceUtil;
013import jmri.jmrit.logixng.util.parser.*;
014import jmri.util.TypeConversionUtil;
015
016/**
017 * Executes an action when the expression is True.
018 *
019 * @author Daniel Bergqvist Copyright 2018
020 */
021public class TableForEach extends AbstractDigitalAction
022        implements FemaleSocketListener, PropertyChangeListener {
023
024    private final LogixNG_SelectNamedBean<NamedTable> _selectNamedBean =
025            new LogixNG_SelectNamedBean<>(
026                    this, NamedTable.class, InstanceManager.getDefault(NamedTableManager.class), this);
027    private NamedBeanAddressing _rowOrColumnAddressing = NamedBeanAddressing.Direct;
028    private TableRowOrColumn _tableRowOrColumn = TableRowOrColumn.Row;
029    private String _rowOrColumnName = "";
030    private String _rowOrColumnReference = "";
031    private String _rowOrColumnLocalVariable = "";
032    private String _rowOrColumnFormula = "";
033    private ExpressionNode _rowOrColumnExpressionNode;
034    private String _variableName = "";
035    private String _socketSystemName;
036    private final FemaleDigitalActionSocket _socket;
037
038    public TableForEach(String sys, String user) {
039        super(sys, user);
040        _socket = InstanceManager.getDefault(DigitalActionManager.class)
041                .createFemaleSocket(this, this, "A1");
042    }
043
044    @Override
045    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
046        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
047        String sysName = systemNames.get(getSystemName());
048        String userName = userNames.get(getSystemName());
049        if (sysName == null) sysName = manager.getAutoSystemName();
050        TableForEach copy = new TableForEach(sysName, userName);
051        copy.setComment(getComment());
052        _selectNamedBean.copy(copy._selectNamedBean);
053        copy.setRowOrColumnAddressing(_rowOrColumnAddressing);
054        copy.setRowOrColumn(_tableRowOrColumn);
055        copy.setRowOrColumnName(_rowOrColumnName);
056        copy.setLocalVariableName(_variableName);
057        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
058    }
059
060    public LogixNG_SelectNamedBean<NamedTable> getSelectNamedBean() {
061        return _selectNamedBean;
062    }
063
064    /** {@inheritDoc} */
065    @Override
066    public Category getCategory() {
067        return Category.FLOW_CONTROL;
068    }
069
070    private String getNewRowOrColumnName() throws JmriException {
071
072        switch (_rowOrColumnAddressing) {
073            case Direct:
074                return _rowOrColumnName;
075
076            case Reference:
077                return ReferenceUtil.getReference(
078                        getConditionalNG().getSymbolTable(), _rowOrColumnReference);
079
080            case LocalVariable:
081                SymbolTable symbolTable = getConditionalNG().getSymbolTable();
082                return TypeConversionUtil
083                        .convertToString(symbolTable.getValue(_rowOrColumnLocalVariable), false);
084
085            case Formula:
086                return _rowOrColumnExpressionNode != null
087                        ? TypeConversionUtil.convertToString(
088                                _rowOrColumnExpressionNode.calculate(
089                                        getConditionalNG().getSymbolTable()), false)
090                        : null;
091
092            default:
093                throw new IllegalArgumentException("invalid _rowOrColumnAddressing state: " + _rowOrColumnAddressing.name());
094        }
095    }
096
097    /** {@inheritDoc} */
098    @Override
099    public void execute() throws JmriException {
100        Table table = _selectNamedBean.evaluateNamedBean(getConditionalNG());
101
102        if (table == null) {
103            return;
104        }
105
106        String rowOrColumnName = getNewRowOrColumnName();
107
108        if (rowOrColumnName == null) {
109            log.error("rowOrColumnName is null");
110            return;
111        }
112        if (_variableName == null) {
113            log.error("variableName is null");
114            return;
115        }
116        if (!_socket.isConnected()) {
117            log.error("socket is not connected");
118            return;
119        }
120
121        SymbolTable symbolTable = getConditionalNG().getSymbolTable();
122
123        if (_tableRowOrColumn == TableRowOrColumn.Row) {
124            int row = 0;    // Empty row name is header row
125            if (!rowOrColumnName.isEmpty()) {
126                row = table.getRowNumber(rowOrColumnName);
127            }
128            for (int column=1; column <= table.numColumns(); column++) {
129                // If the header is null or empty, treat the row as a comment
130                Object header = table.getCell(0, column);
131                if ((header != null) && (!header.toString().isEmpty())) {
132                    symbolTable.setValue(_variableName, table.getCell(row, column));
133                    try {
134                        _socket.execute();
135                    } catch (BreakException e) {
136                        break;
137                    } catch (ContinueException e) {
138                        // Do nothing, just catch it.
139                    }
140                }
141            }
142        } else {
143            int column = 0;    // Empty column name is header column
144            if (!rowOrColumnName.isEmpty()) {
145                column = table.getColumnNumber(rowOrColumnName);
146            }
147            for (int row=1; row <= table.numRows(); row++) {
148                // If the header is null or empty, treat the row as a comment
149                Object header = table.getCell(row, 0);
150//                System.out.format("Column header: %s%n", header);
151                if ((header != null) && (!header.toString().isEmpty())) {
152                    symbolTable.setValue(_variableName, table.getCell(row, column));
153//                    System.out.format("Variable: %s, value: %s%n", _variableName, table.getCell(row, column));
154                    try {
155                        _socket.execute();
156                    } catch (BreakException e) {
157                        break;
158                    } catch (ContinueException e) {
159                        // Do nothing, just catch it.
160                    }
161                }
162            }
163        }
164    }
165
166    /**
167     * Get tableRowOrColumn.
168     * @return tableRowOrColumn
169     */
170    public TableRowOrColumn getRowOrColumn() {
171        return _tableRowOrColumn;
172    }
173
174    /**
175     * Set tableRowOrColumn.
176     * @param tableRowOrColumn tableRowOrColumn
177     */
178    public void setRowOrColumn(@Nonnull TableRowOrColumn tableRowOrColumn) {
179        _tableRowOrColumn = tableRowOrColumn;
180    }
181
182    public void setRowOrColumnAddressing(NamedBeanAddressing addressing) throws ParserException {
183        _rowOrColumnAddressing = addressing;
184        parseRowOrColumnFormula();
185    }
186
187    public NamedBeanAddressing getRowOrColumnAddressing() {
188        return _rowOrColumnAddressing;
189    }
190
191    /**
192     * Get name of row or column
193     * @return name of row or column
194     */
195    public String getRowOrColumnName() {
196        return _rowOrColumnName;
197    }
198
199    /**
200     * Set name of row or column
201     * @param rowOrColumnName name of row or column
202     */
203    public void setRowOrColumnName(@Nonnull String rowOrColumnName) {
204        if (rowOrColumnName == null) throw new RuntimeException("Daniel");
205        _rowOrColumnName = rowOrColumnName;
206    }
207
208    public void setRowOrColumnReference(@Nonnull String reference) {
209        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
210            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
211        }
212        _rowOrColumnReference = reference;
213    }
214
215    public String getRowOrColumnReference() {
216        return _rowOrColumnReference;
217    }
218
219    public void setRowOrColumnLocalVariable(@Nonnull String localVariable) {
220        _rowOrColumnLocalVariable = localVariable;
221    }
222
223    public String getRowOrColumnLocalVariable() {
224        return _rowOrColumnLocalVariable;
225    }
226
227    public void setRowOrColumnFormula(@Nonnull String formula) throws ParserException {
228        _rowOrColumnFormula = formula;
229        parseRowOrColumnFormula();
230    }
231
232    public String getRowOrColumnFormula() {
233        return _rowOrColumnFormula;
234    }
235
236    private void parseRowOrColumnFormula() throws ParserException {
237        if (_rowOrColumnAddressing == NamedBeanAddressing.Formula) {
238            Map<String, Variable> variables = new HashMap<>();
239
240            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
241            _rowOrColumnExpressionNode = parser.parseExpression(_rowOrColumnFormula);
242        } else {
243            _rowOrColumnExpressionNode = null;
244        }
245    }
246
247    /**
248     * Get name of local variable
249     * @return name of local variable
250     */
251    public String getLocalVariableName() {
252        return _variableName;
253    }
254
255    /**
256     * Set name of local variable
257     * @param localVariableName name of local variable
258     */
259    public void setLocalVariableName(String localVariableName) {
260        _variableName = localVariableName;
261    }
262
263    @Override
264    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
265        switch (index) {
266            case 0:
267                return _socket;
268
269            default:
270                throw new IllegalArgumentException(
271                        String.format("index has invalid value: %d", index));
272        }
273    }
274
275    @Override
276    public int getChildCount() {
277        return 1;
278    }
279
280    @Override
281    public void connected(FemaleSocket socket) {
282        if (socket == _socket) {
283            _socketSystemName = socket.getConnectedSocket().getSystemName();
284        } else {
285            throw new IllegalArgumentException("unkown socket");
286        }
287    }
288
289    @Override
290    public void disconnected(FemaleSocket socket) {
291        if (socket == _socket) {
292            _socketSystemName = null;
293        } else {
294            throw new IllegalArgumentException("unkown socket");
295        }
296    }
297
298    @Override
299    public String getShortDescription(Locale locale) {
300        return Bundle.getMessage(locale, "TableForEach_Short");
301    }
302
303    @Override
304    public String getLongDescription(Locale locale) {
305        String namedBean = _selectNamedBean.getDescription(locale);
306        String rowOrColumnName;
307
308        switch (_rowOrColumnAddressing) {
309            case Direct:
310                String name = _rowOrColumnName;
311                if (name.isEmpty()) name = Bundle.getMessage("TableForEach_Header");
312                rowOrColumnName = Bundle.getMessage(locale, "AddressByDirect", name);
313                break;
314
315            case Reference:
316                rowOrColumnName = Bundle.getMessage(locale, "AddressByReference", _rowOrColumnReference);
317                break;
318
319            case LocalVariable:
320                rowOrColumnName = Bundle.getMessage(locale, "AddressByLocalVariable", _rowOrColumnLocalVariable);
321                break;
322
323            case Formula:
324                rowOrColumnName = Bundle.getMessage(locale, "AddressByFormula", _rowOrColumnFormula);
325                break;
326
327            default:
328                throw new IllegalArgumentException("invalid _rowOrColumnAddressing state: " + _rowOrColumnAddressing.name());
329        }
330
331        return Bundle.getMessage(locale, "TableForEach_Long",
332                _tableRowOrColumn.getOpposite().toStringLowerCase(),
333                _tableRowOrColumn.toStringLowerCase(),
334                rowOrColumnName,
335                namedBean,
336                _variableName,
337                _socket.getName());
338    }
339
340    public FemaleDigitalActionSocket getSocket() {
341        return _socket;
342    }
343
344    public String getSocketSystemName() {
345        return _socketSystemName;
346    }
347
348    public void setSocketSystemName(String systemName) {
349        _socketSystemName = systemName;
350    }
351
352    /** {@inheritDoc} */
353    @Override
354    public void setup() {
355        try {
356            if ( !_socket.isConnected()
357                    || !_socket.getConnectedSocket().getSystemName()
358                            .equals(_socketSystemName)) {
359
360                String socketSystemName = _socketSystemName;
361                _socket.disconnect();
362                if (socketSystemName != null) {
363                    MaleSocket maleSocket =
364                            InstanceManager.getDefault(DigitalActionManager.class)
365                                    .getBySystemName(socketSystemName);
366                    _socket.disconnect();
367                    if (maleSocket != null) {
368                        _socket.connect(maleSocket);
369                        maleSocket.setup();
370                    } else {
371                        log.error("cannot load digital action {}", socketSystemName);
372                    }
373                }
374            } else {
375                _socket.getConnectedSocket().setup();
376            }
377        } catch (SocketAlreadyConnectedException ex) {
378            // This shouldn't happen and is a runtime error if it does.
379            throw new RuntimeException("socket is already connected");
380        }
381    }
382
383    /** {@inheritDoc} */
384    @Override
385    public void registerListenersForThisClass() {
386        _selectNamedBean.registerListeners();
387    }
388
389    /** {@inheritDoc} */
390    @Override
391    public void unregisterListenersForThisClass() {
392        _selectNamedBean.unregisterListeners();
393    }
394
395    /** {@inheritDoc} */
396    @Override
397    public void disposeMe() {
398    }
399
400    /** {@inheritDoc} */
401    @Override
402    public void propertyChange(PropertyChangeEvent evt) {
403        getConditionalNG().execute();
404    }
405
406    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TableForEach.class);
407
408}