001package jmri.jmrit.logixng.implementation;
002
003import java.awt.GraphicsEnvironment;
004import java.beans.*;
005import java.io.PrintWriter;
006import java.util.*;
007
008import javax.annotation.Nonnull;
009import javax.annotation.OverridingMethodsMustInvokeSuper;
010
011import jmri.*;
012import jmri.jmrit.logixng.*;
013import jmri.jmrit.logixng.Base.PrintTreeSettings;
014import jmri.jmrit.logixng.Module;
015import jmri.managers.AbstractManager;
016import jmri.util.LoggingUtil;
017import jmri.util.ThreadingUtil;
018import jmri.util.swing.JmriJOptionPane;
019
020import org.apache.commons.lang3.mutable.MutableInt;
021
022/**
023 * Class providing the basic logic of the LogixNG_Manager interface.
024 *
025 * @author Dave Duchamp       Copyright (C) 2007
026 * @author Daniel Bergqvist   Copyright (C) 2018
027 */
028public class DefaultLogixNGManager extends AbstractManager<LogixNG>
029        implements LogixNG_Manager {
030
031
032    private final Map<String, Manager<? extends MaleSocket>> _managers = new HashMap<>();
033    private final Clipboard _clipboard = new DefaultClipboard();
034    private boolean _isActive = false;
035    private boolean _startLogixNGsOnLoad = true;
036    private boolean _loadDisabled = false;
037    private final List<Runnable> _setupTasks = new ArrayList<>();
038
039
040    public DefaultLogixNGManager() {
041        // The LogixNGPreferences class may load plugins so we must ensure
042        // it's loaded here.
043        InstanceManager.getDefault(LogixNGPreferences.class);
044    }
045
046    @Override
047    public int getXMLOrder() {
048        return LOGIXNGS;
049    }
050
051    @Override
052    public char typeLetter() {
053        return 'Q';
054    }
055
056    /**
057     * Test if parameter is a properly formatted system name.
058     *
059     * @param systemName the system name
060     * @return enum indicating current validity, which might be just as a prefix
061     */
062    @Override
063    public NameValidity validSystemNameFormat(String systemName) {
064        return LogixNG_Manager.validSystemNameFormat(
065                getSubSystemNamePrefix(), systemName);
066//        if (systemName.matches(getSubSystemNamePrefix()+"(:AUTO:)?\\d+")) {
067//            return NameValidity.VALID;
068//        } else {
069//            return NameValidity.INVALID;
070//        }
071    }
072
073    /**
074     * Method to create a new LogixNG if the LogixNG does not exist.
075     * <p>
076     * Returns null if
077     * a Logix with the same systemName or userName already exists, or if there
078     * is trouble creating a new LogixNG.
079     */
080    @Override
081    public LogixNG createLogixNG(String systemName, String userName)
082            throws IllegalArgumentException {
083        return createLogixNG(systemName, userName, false);
084    }
085
086    /**
087     * Method to create a new LogixNG if the LogixNG does not exist.
088     * <p>
089     * Returns null if
090     * a Logix with the same systemName or userName already exists, or if there
091     * is trouble creating a new LogixNG.
092     */
093    @Override
094    public LogixNG createLogixNG(String systemName, String userName, boolean inline)
095            throws IllegalArgumentException {
096
097        // Check that LogixNG does not already exist
098        LogixNG x;
099        if (userName != null && !userName.equals("")) {
100            x = getByUserName(userName);
101            if (x != null) {
102                return null;
103            }
104        }
105        x = getBySystemName(systemName);
106        if (x != null) {
107            return null;
108        }
109        // Check if system name is valid
110        if (this.validSystemNameFormat(systemName) != NameValidity.VALID) {
111            throw new IllegalArgumentException("SystemName " + systemName + " is not in the correct format");
112        }
113        // LogixNG does not exist, create a new LogixNG
114        x = new DefaultLogixNG(systemName, userName, inline);
115        // save in the maps
116        register(x);
117
118        // Keep track of the last created auto system name
119        updateAutoNumber(systemName);
120
121        return x;
122    }
123
124    @Override
125    public LogixNG createLogixNG(String userName) throws IllegalArgumentException {
126        return createLogixNG(getAutoSystemName(), userName);
127    }
128
129    @Override
130    public LogixNG createLogixNG(String userName, boolean inline)
131            throws IllegalArgumentException {
132        return createLogixNG(getAutoSystemName(), userName, inline);
133    }
134
135    @Override
136    public LogixNG getLogixNG(String name) {
137        LogixNG x = getByUserName(name);
138        if (x != null) {
139            return x;
140        }
141        return getBySystemName(name);
142    }
143
144    @Override
145    public LogixNG getByUserName(String name) {
146        return _tuser.get(name);
147    }
148
149    @Override
150    public LogixNG getBySystemName(String name) {
151        return _tsys.get(name);
152    }
153
154    /** {@inheritDoc} */
155    @Override
156    public String getBeanTypeHandled(boolean plural) {
157        return Bundle.getMessage(plural ? "BeanNameLogixNGs" : "BeanNameLogixNG");
158    }
159
160    /** {@inheritDoc} */
161    @Override
162    public void setLoadDisabled(boolean value) {
163        _loadDisabled = value;
164    }
165
166    /** {@inheritDoc} */
167    @Override
168    public void startLogixNGsOnLoad(boolean value) {
169        _startLogixNGsOnLoad = value;
170    }
171
172    /** {@inheritDoc} */
173    @Override
174    public boolean isStartLogixNGsOnLoad() {
175        return _startLogixNGsOnLoad;
176    }
177
178    /** {@inheritDoc} */
179    @Override
180    public void setupAllLogixNGs() {
181        List<String> errors = new ArrayList<>();
182        boolean result = true;
183        for (LogixNG logixNG : _tsys.values()) {
184            logixNG.setup();
185            result = result && logixNG.setParentForAllChildren(errors);
186        }
187        for (Module module : InstanceManager.getDefault(ModuleManager.class).getNamedBeanSet()) {
188            module.setup();
189            result = result && module.setParentForAllChildren(errors);
190        }
191        _clipboard.setup();
192        for (Runnable r : _setupTasks) {
193            r.run();
194        }
195        if (errors.size() > 0) {
196            messageDialog("SetupErrorsTitle", errors, null);
197        }
198        checkItemsHaveParents();
199    }
200
201    /**
202     * Display LogixNG setup errors when not running in headless mode.
203     * @param titleKey The bundle key for the dialog title.
204     * @param messages A ArrayList of messages that have been localized.
205     * @param helpKey The bundle key for additional information about the errors
206     */
207    private void messageDialog(String titleKey, List<String> messages, String helpKey) {
208        if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("jmri.test.no-dialogs")) {
209            StringBuilder sb = new StringBuilder("<html>");
210            messages.forEach(msg -> {
211                sb.append(msg);
212                sb.append("<br>");
213            });
214            if (helpKey != null) {
215                sb.append("<br>");
216                sb.append(Bundle.getMessage(helpKey));
217            }
218            sb.append("/<html>");
219            JmriJOptionPane.showMessageDialog(null,
220                    sb.toString(),
221                    Bundle.getMessage(titleKey),
222                    JmriJOptionPane.WARNING_MESSAGE);
223        }
224    }
225
226    private void checkItemsHaveParents(SortedSet<? extends MaleSocket> set, List<MaleSocket> beansWithoutParentList) {
227        for (MaleSocket bean : set) {
228            if (bean.getParent() == null) {
229                beansWithoutParentList.add(bean);
230            }
231        }
232    }
233
234    private void checkItemsHaveParents() {
235        List<MaleSocket> beansWithoutParentList = new ArrayList<>();
236        checkItemsHaveParents(InstanceManager.getDefault(AnalogActionManager.class).getNamedBeanSet(), beansWithoutParentList);
237        checkItemsHaveParents(InstanceManager.getDefault(DigitalActionManager.class).getNamedBeanSet(), beansWithoutParentList);
238        checkItemsHaveParents(InstanceManager.getDefault(DigitalBooleanActionManager.class).getNamedBeanSet(), beansWithoutParentList);
239        checkItemsHaveParents(InstanceManager.getDefault(StringActionManager.class).getNamedBeanSet(), beansWithoutParentList);
240        checkItemsHaveParents(InstanceManager.getDefault(AnalogExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
241        checkItemsHaveParents(InstanceManager.getDefault(DigitalExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
242        checkItemsHaveParents(InstanceManager.getDefault(StringExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
243
244        if (!beansWithoutParentList.isEmpty()) {
245            List<String> errors = new ArrayList<>();
246            List<String> msgs = new ArrayList<>();
247            for (Base b : beansWithoutParentList) {
248                b.setup();
249                b.setParentForAllChildren(errors);
250            }
251            for (Base b : beansWithoutParentList) {
252                if (b.getParent() == null) {
253                    log.error("Item has no parent: {}, {}, {}",
254                            b.getSystemName(),
255                            b.getUserName(),
256                            b.getLongDescription());
257                    msgs.add(Bundle.getMessage("NoParentMessage",
258                            b.getSystemName(),
259                            b.getUserName(),
260                            b.getLongDescription()));
261
262                    for (int i=0; i < b.getChildCount(); i++) {
263                        if (b.getChild(i).isConnected()) {
264                            log.error("    Child: {}, {}, {}",
265                                    b.getChild(i).getConnectedSocket().getSystemName(),
266                                    b.getChild(i).getConnectedSocket().getUserName(),
267                                    b.getChild(i).getConnectedSocket().getLongDescription());
268                        }
269                    }
270                    log.error("                                             End Item");
271                    List<String> cliperrors = new ArrayList<String>();
272                    _clipboard.add((MaleSocket) b, cliperrors);
273                }
274            }
275            messageDialog("ParentErrorsTitle", msgs, "NoParentHelp");
276        }
277    }
278
279    /** {@inheritDoc} */
280    @Override
281    public void activateAllLogixNGs() {
282        activateAllLogixNGs(true, true);
283    }
284
285    /** {@inheritDoc} */
286    @Override
287    public void activateAllLogixNGs(boolean runDelayed, boolean runOnSeparateThread) {
288
289        _isActive = true;
290
291        if (_loadDisabled) {
292            for (LogixNG logixNG : _tsys.values()) {
293                logixNG.setEnabled(false);
294            }
295            _loadDisabled = false;
296        }
297
298        // This may take a long time so it must not be done on the GUI thread.
299        // Therefore we create a new thread for this task.
300        Runnable runnable = () -> {
301
302            // Initialize the values of the global variables
303            Set<GlobalVariable> globalVariables =
304                    InstanceManager.getDefault(GlobalVariableManager.class)
305                            .getNamedBeanSet();
306
307            for (GlobalVariable gv : globalVariables) {
308                try {
309                    gv.initialize();
310                } catch (JmriException | IllegalArgumentException e) {
311                    log.warn("Variable {} could not be initialized", gv.getUserName(), e);
312                }
313            }
314
315            Set<LogixNG> activeLogixNGs = new HashSet<>();
316
317            // Activate and execute the initialization LogixNGs first.
318            List<LogixNG> initLogixNGs =
319                    InstanceManager.getDefault(LogixNG_InitializationManager.class)
320                            .getList();
321
322            for (LogixNG logixNG : initLogixNGs) {
323                logixNG.activate();
324                if (logixNG.isActive()) {
325                    logixNG.registerListeners();
326                    logixNG.execute(false, true);
327                    activeLogixNGs.add(logixNG);
328                } else {
329                    logixNG.unregisterListeners();
330                }
331            }
332
333            // Activate and execute all the rest of the LogixNGs.
334            _tsys.values().stream()
335                    .sorted()
336                    .filter((logixNG) -> !(activeLogixNGs.contains(logixNG)))
337                    .forEachOrdered((logixNG) -> {
338
339                logixNG.activate();
340
341                if (logixNG.isActive()) {
342                    logixNG.registerListeners();
343                    logixNG.execute(true, true);
344                } else {
345                    logixNG.unregisterListeners();
346                }
347            });
348
349            // Clear the startup flag of the LogixNGs.
350            _tsys.values().stream().forEach((logixNG) -> {
351                logixNG.clearStartup();
352            });
353        };
354
355        if (runOnSeparateThread) new Thread(runnable).start();
356        else runnable.run();
357    }
358
359    /** {@inheritDoc} */
360    @Override
361    public void deActivateAllLogixNGs() {
362        for (LogixNG logixNG : _tsys.values()) {
363            logixNG.unregisterListeners();
364        }
365        _isActive = false;
366    }
367
368    /** {@inheritDoc} */
369    @Override
370    public boolean isActive() {
371        return _isActive;
372    }
373
374    /** {@inheritDoc} */
375    @Override
376    public void deleteLogixNG(LogixNG x) {
377        // delete the LogixNG
378        deregister(x);
379        x.dispose();
380    }
381
382    /** {@inheritDoc} */
383    @Override
384    public void printTree(
385            PrintTreeSettings settings,
386            PrintWriter writer,
387            String indent,
388            MutableInt lineNumber) {
389
390        printTree(settings, Locale.getDefault(), writer, indent, lineNumber);
391    }
392
393    /** {@inheritDoc} */
394    @Override
395    public void printTree(
396            PrintTreeSettings settings,
397            Locale locale,
398            PrintWriter writer,
399            String indent,
400            MutableInt lineNumber) {
401
402        for (LogixNG logixNG : getNamedBeanSet()) {
403            if (logixNG.isInline()) continue;
404            logixNG.printTree(settings, locale, writer, indent, "", lineNumber);
405            writer.println();
406        }
407
408        for (LogixNG logixNG : getNamedBeanSet()) {
409            if (!logixNG.isInline()) continue;
410            logixNG.printTree(settings, locale, writer, indent, "", lineNumber);
411            writer.println();
412        }
413        InstanceManager.getDefault(ModuleManager.class).printTree(settings, locale, writer, indent, lineNumber);
414        InstanceManager.getDefault(NamedTableManager.class).printTree(locale, writer, indent);
415        InstanceManager.getDefault(GlobalVariableManager.class).printTree(locale, writer, indent);
416        InstanceManager.getDefault(LogixNG_InitializationManager.class).printTree(locale, writer, indent);
417    }
418
419
420    static volatile DefaultLogixNGManager _instance = null;
421
422    @InvokeOnGuiThread  // this method is not thread safe
423    static public DefaultLogixNGManager instance() {
424        if (!ThreadingUtil.isGUIThread()) {
425            LoggingUtil.warnOnce(log, "instance() called on wrong thread");
426        }
427
428        if (_instance == null) {
429            _instance = new DefaultLogixNGManager();
430        }
431        return (_instance);
432    }
433
434    /** {@inheritDoc} */
435    @Override
436    public Class<LogixNG> getNamedBeanClass() {
437        return LogixNG.class;
438    }
439
440    /** {@inheritDoc} */
441    @Override
442    public Clipboard getClipboard() {
443        return _clipboard;
444    }
445
446    /** {@inheritDoc} */
447    @Override
448    public void registerManager(Manager<? extends MaleSocket> manager) {
449        _managers.put(manager.getClass().getName(), manager);
450    }
451
452    /** {@inheritDoc} */
453    @Override
454    public Manager<? extends MaleSocket> getManager(String className) {
455        return _managers.get(className);
456    }
457
458    /**
459     * Inform all registered listeners of a vetoable change.If the propertyName
460     * is "CanDelete" ALL listeners with an interest in the bean will throw an
461     * exception, which is recorded returned back to the invoking method, so
462     * that it can be presented back to the user.However if a listener decides
463     * that the bean can not be deleted then it should throw an exception with
464     * a property name of "DoNotDelete", this is thrown back up to the user and
465     * the delete process should be aborted.
466     *
467     * @param p   The programmatic name of the property that is to be changed.
468     *            "CanDelete" will inquire with all listeners if the item can
469     *            be deleted. "DoDelete" tells the listener to delete the item.
470     * @param old The old value of the property.
471     * @throws java.beans.PropertyVetoException If the recipients wishes the
472     *                                          delete to be aborted (see above)
473     */
474    @OverridingMethodsMustInvokeSuper
475    public void fireVetoableChange(String p, Object old) throws PropertyVetoException {
476        PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, null);
477        for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) {
478            vc.vetoableChange(evt);
479        }
480    }
481
482    /** {@inheritDoc} */
483    @Override
484//    @OverridingMethodsMustInvokeSuper
485    public final void deleteBean(@Nonnull LogixNG logixNG, @Nonnull String property) throws PropertyVetoException {
486        for (int i=logixNG.getNumConditionalNGs()-1; i >= 0; i--) {
487            ConditionalNG child = logixNG.getConditionalNG(i);
488            InstanceManager.getDefault(ConditionalNG_Manager.class).deleteBean(child, property);
489        }
490
491        // throws PropertyVetoException if vetoed
492        fireVetoableChange(property, logixNG);
493        if (property.equals("DoDelete")) { // NOI18N
494            deregister(logixNG);
495            logixNG.dispose();
496        }
497    }
498
499    /** {@inheritDoc} */
500    @Override
501    public void registerSetupTask(Runnable task) {
502        _setupTasks.add(task);
503    }
504
505    /** {@inheritDoc} */
506    @Override
507    public void executeModule(Module module, Object parameter)
508            throws IllegalArgumentException {
509
510        if (module == null) {
511            throw new IllegalArgumentException("The parameter \"module\" is null");
512        }
513        // Get the parameters for the module
514        Collection<Module.Parameter> parameterNames = module.getParameters();
515
516        // Ensure that there is only one parameter
517        if (parameterNames.size() != 1) {
518            throw new IllegalArgumentException("The module doesn't take exactly one parameter");
519        }
520
521        // Get the parameter
522        Module.Parameter param = parameterNames.toArray(Module.Parameter[]::new)[0];
523        if (!param.isInput()) {
524            throw new IllegalArgumentException("The module's parameter is not an input parameter");
525        }
526
527        // Set the value of the parameter
528        Map<String, Object> parameters = new HashMap<>();
529        parameters.put(param.getName(), parameter);
530
531        // Execute the module
532        executeModule(module, parameters);
533    }
534
535    /** {@inheritDoc} */
536    @Override
537    public void executeModule(Module module, Map<String, Object> parameters)
538            throws IllegalArgumentException {
539        DefaultConditionalNG.executeModule(module, parameters);
540    }
541
542    /**
543     * The PropertyChangeListener interface in this class is intended to keep
544     * track of user name changes to individual NamedBeans. It is not completely
545     * implemented yet. In particular, listeners are not added to newly
546     * registered objects.
547     *
548     * @param e the event
549     */
550    @Override
551    @OverridingMethodsMustInvokeSuper
552    public void propertyChange(PropertyChangeEvent e) {
553        super.propertyChange(e);
554        if (LogixNG.PROPERTY_INLINE.equals(e.getPropertyName())) {
555            // If a LogixNG changes its "inline" state, the number of items
556            // listed in the LogixNG table might change.
557            firePropertyChange("length", null, _beans.size());
558        }
559    }
560
561
562    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultLogixNGManager.class);
563
564}