001package jmri.jmrit.logixng.actions;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006import java.util.concurrent.atomic.AtomicReference;
007
008import jmri.*;
009import jmri.jmrit.logixng.*;
010import jmri.jmrit.logixng.util.*;
011import jmri.jmrit.logixng.util.parser.*;
012import jmri.util.ThreadingUtil;
013
014/**
015 * Executes an action when the expression is True.
016 *
017 * @author Daniel Bergqvist Copyright 2018
018 */
019public class ForEach extends AbstractDigitalAction
020        implements FemaleSocketListener, PropertyChangeListener {
021
022    private final LogixNG_SelectString _selectVariable =
023            new LogixNG_SelectString(this, this);
024
025    private final LogixNG_SelectNamedBean<Memory> _selectMemoryNamedBean =
026            new LogixNG_SelectNamedBean<>(
027                    this, Memory.class, InstanceManager.getDefault(MemoryManager.class), this);
028
029    private boolean _useCommonSource = true;
030    private CommonManager _commonManager = CommonManager.Sensors;
031    private UserSpecifiedSource _userSpecifiedSource = UserSpecifiedSource.Variable;
032    private String _formula = "";
033    private ExpressionNode _expressionNode;
034    private String _variableName = "";
035    private String _socketSystemName;
036    private final FemaleDigitalActionSocket _socket;
037
038    public ForEach(String sys, String user) {
039        super(sys, user);
040        _socket = InstanceManager.getDefault(DigitalActionManager.class)
041                .createFemaleSocket(this, this, "A");
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        ForEach copy = new ForEach(sysName, userName);
051        copy.setComment(getComment());
052        copy.setUseCommonSource(_useCommonSource);
053        copy.setCommonManager(_commonManager);
054        copy.setUserSpecifiedSource(_userSpecifiedSource);
055        _selectVariable.copy(copy._selectVariable);
056        _selectMemoryNamedBean.copy(copy._selectMemoryNamedBean);
057        copy.setFormula(_formula);
058        copy.setLocalVariableName(_variableName);
059        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
060    }
061
062    public LogixNG_SelectString getSelectVariable() {
063        return _selectVariable;
064    }
065
066    public LogixNG_SelectNamedBean<Memory> getSelectMemoryNamedBean() {
067        return _selectMemoryNamedBean;
068    }
069
070    public void setUseCommonSource(boolean commonSource) {
071        this._useCommonSource = commonSource;
072    }
073
074    public boolean isUseCommonSource() {
075        return _useCommonSource;
076    }
077
078    public void setCommonManager(CommonManager commonManager) throws ParserException {
079        _commonManager = commonManager;
080        parseFormula();
081    }
082
083    public CommonManager getCommonManager() {
084        return _commonManager;
085    }
086
087    public void setUserSpecifiedSource(UserSpecifiedSource userSpecifiedSource) throws ParserException {
088        _userSpecifiedSource = userSpecifiedSource;
089        parseFormula();
090    }
091
092    public UserSpecifiedSource getUserSpecifiedSource() {
093        return _userSpecifiedSource;
094    }
095
096    public void setFormula(String formula) throws ParserException {
097        _formula = formula;
098        parseFormula();
099    }
100
101    public String getFormula() {
102        return _formula;
103    }
104
105    private void parseFormula() throws ParserException {
106        if (_userSpecifiedSource == UserSpecifiedSource.Formula) {
107            Map<String, Variable> variables = new HashMap<>();
108
109            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
110            _expressionNode = parser.parseExpression(_formula);
111        } else {
112            _expressionNode = null;
113        }
114    }
115
116    /**
117     * Get name of local variable
118     * @return name of local variable
119     */
120    public String getLocalVariableName() {
121        return _variableName;
122    }
123
124    /**
125     * Set name of local variable
126     * @param localVariableName name of local variable
127     */
128    public void setLocalVariableName(String localVariableName) {
129        _variableName = localVariableName;
130    }
131
132    /** {@inheritDoc} */
133    @Override
134    public Category getCategory() {
135        return Category.FLOW_CONTROL;
136    }
137
138    /** {@inheritDoc} */
139    @Override
140    @SuppressWarnings("unchecked")
141    public void execute() throws JmriException {
142        SymbolTable symbolTable = getConditionalNG().getSymbolTable();
143
144        AtomicReference<Collection<? extends Object>> collectionRef = new AtomicReference<>();
145        AtomicReference<JmriException> ref = new AtomicReference<>();
146
147        final ConditionalNG conditionalNG = getConditionalNG();
148
149        if (_useCommonSource) {
150            collectionRef.set(_commonManager.getManager().getNamedBeanSet());
151        } else {
152            ThreadingUtil.runOnLayoutWithJmriException(() -> {
153
154                Object value = null;
155
156                switch (_userSpecifiedSource) {
157                    case Variable:
158                        String otherLocalVariable = _selectVariable.evaluateValue(getConditionalNG());
159                        Object variableValue = conditionalNG
160                                        .getSymbolTable().getValue(otherLocalVariable);
161
162                        value = variableValue;
163                        break;
164
165                    case Memory:
166                        Memory memory = _selectMemoryNamedBean.evaluateNamedBean(getConditionalNG());
167                        if (memory != null) {
168                            value = memory.getValue();
169                        } else {
170                            log.warn("ForEach memory is null");
171                        }
172                        break;
173
174                    case Formula:
175                        if (!_formula.isEmpty() && _expressionNode != null) {
176                            value = _expressionNode.calculate(conditionalNG.getSymbolTable());
177                        }
178                        break;
179
180                    default:
181                        // Throw exception
182                        throw new IllegalArgumentException("_userSpecifiedSource has invalid value: {}" + _userSpecifiedSource.name());
183                }
184
185                if (value instanceof Manager) {
186                    collectionRef.set(((Manager<? extends NamedBean>) value).getNamedBeanSet());
187                } else if (value != null && value.getClass().isArray()) {
188                    // Note: (Object[]) is needed to tell that the parameter is an array and not a vararg argument
189                    // See: https://stackoverflow.com/questions/2607289/converting-array-to-list-in-java/2607327#2607327
190                    collectionRef.set(Arrays.asList((Object[])value));
191                } else if (value instanceof Collection) {
192                    collectionRef.set((Collection<? extends Object>) value);
193                } else if (value instanceof Map) {
194                    collectionRef.set(((Map<?,?>) value).entrySet());
195                } else {
196                    throw new JmriException(Bundle.getMessage("ForEach_InvalidValue",
197                                    value != null ? value.getClass().getName() : null));
198                }
199            });
200        }
201
202        if (ref.get() != null) throw ref.get();
203
204        for (Object o : collectionRef.get()) {
205            symbolTable.setValue(_variableName, o);
206            try {
207                _socket.execute();
208            } catch (BreakException e) {
209                break;
210            } catch (ContinueException e) {
211                // Do nothing, just catch it.
212            }
213        }
214    }
215
216    @Override
217    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
218        switch (index) {
219            case 0:
220                return _socket;
221
222            default:
223                throw new IllegalArgumentException(
224                        String.format("index has invalid value: %d", index));
225        }
226    }
227
228    @Override
229    public int getChildCount() {
230        return 1;
231    }
232
233    @Override
234    public void connected(FemaleSocket socket) {
235        if (socket == _socket) {
236            _socketSystemName = socket.getConnectedSocket().getSystemName();
237        } else {
238            throw new IllegalArgumentException("unkown socket");
239        }
240    }
241
242    @Override
243    public void disconnected(FemaleSocket socket) {
244        if (socket == _socket) {
245            _socketSystemName = null;
246        } else {
247            throw new IllegalArgumentException("unkown socket");
248        }
249    }
250
251    @Override
252    public String getShortDescription(Locale locale) {
253        return Bundle.getMessage(locale, "ForEach_Short");
254    }
255
256    @Override
257    public String getLongDescription(Locale locale) {
258        if (_useCommonSource) {
259            return Bundle.getMessage(locale, "ForEach_Long_Common",
260                    _commonManager.toString(), _variableName, _socket.getName());
261        } else {
262            switch (_userSpecifiedSource) {
263                case Variable:
264                    return Bundle.getMessage(locale, "ForEach_Long_LocalVariable",
265                            _selectVariable.getDescription(locale), _variableName, _socket.getName());
266
267                case Memory:
268                    return Bundle.getMessage(locale, "ForEach_Long_Memory",
269                            _selectMemoryNamedBean.getDescription(locale), _variableName, _socket.getName());
270
271                case Formula:
272                    return Bundle.getMessage(locale, "ForEach_Long_Formula",
273                            _formula, _variableName, _socket.getName());
274
275                default:
276                    throw new IllegalArgumentException("_variableOperation has invalid value: " + _userSpecifiedSource.name());
277            }
278        }
279    }
280
281    public FemaleDigitalActionSocket getSocket() {
282        return _socket;
283    }
284
285    public String getSocketSystemName() {
286        return _socketSystemName;
287    }
288
289    public void setSocketSystemName(String systemName) {
290        _socketSystemName = systemName;
291    }
292
293    /** {@inheritDoc} */
294    @Override
295    public void setup() {
296        try {
297            if ( !_socket.isConnected()
298                    || !_socket.getConnectedSocket().getSystemName()
299                            .equals(_socketSystemName)) {
300
301                String socketSystemName = _socketSystemName;
302                _socket.disconnect();
303                if (socketSystemName != null) {
304                    MaleSocket maleSocket =
305                            InstanceManager.getDefault(DigitalActionManager.class)
306                                    .getBySystemName(socketSystemName);
307                    _socket.disconnect();
308                    if (maleSocket != null) {
309                        _socket.connect(maleSocket);
310                        maleSocket.setup();
311                    } else {
312                        log.error("cannot load digital action {}", socketSystemName);
313                    }
314                }
315            } else {
316                _socket.getConnectedSocket().setup();
317            }
318        } catch (SocketAlreadyConnectedException ex) {
319            // This shouldn't happen and is a runtime error if it does.
320            throw new RuntimeException("socket is already connected");
321        }
322    }
323
324    /** {@inheritDoc} */
325    @Override
326    public void registerListenersForThisClass() {
327        if (!_listenersAreRegistered) {
328            if (_userSpecifiedSource == UserSpecifiedSource.Memory) {
329                _selectMemoryNamedBean.registerListeners();
330            }
331            _listenersAreRegistered = true;
332        }
333    }
334
335    /** {@inheritDoc} */
336    @Override
337    public void unregisterListenersForThisClass() {
338        if (_listenersAreRegistered) {
339            if (_userSpecifiedSource == UserSpecifiedSource.Memory) {
340                _selectMemoryNamedBean.unregisterListeners();
341            }
342            _listenersAreRegistered = false;
343        }
344    }
345
346    /** {@inheritDoc} */
347    @Override
348    public void disposeMe() {
349    }
350
351    /** {@inheritDoc} */
352    @Override
353    public void propertyChange(PropertyChangeEvent evt) {
354        getConditionalNG().execute();
355    }
356
357
358    public enum UserSpecifiedSource {
359        Variable(Bundle.getMessage("ForEach_UserSpecifiedSource_Variable")),
360        Memory(Bundle.getMessage("ForEach_UserSpecifiedSource_Memory")),
361        Formula(Bundle.getMessage("ForEach_UserSpecifiedSource_Formula"));
362
363        private final String _text;
364
365        private UserSpecifiedSource(String text) {
366            this._text = text;
367        }
368
369        @Override
370        public String toString() {
371            return _text;
372        }
373
374    }
375
376
377
378
379    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ForEach.class);
380
381}