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