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