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.isEmpty()) {
196            messageDialog("SetupErrorsTitle", errors, null);
197        }
198        checkItemsHaveParents();
199
200        // Notify listeners that setupAllLogixNGs() is completed.
201        firePropertyChange(PROPERTY_SETUP, false, true);
202    }
203
204    /**
205     * Display LogixNG setup errors when not running in headless mode.
206     * @param titleKey The bundle key for the dialog title.
207     * @param messages A ArrayList of messages that have been localized.
208     * @param helpKey The bundle key for additional information about the errors
209     */
210    private void messageDialog(String titleKey, List<String> messages, String helpKey) {
211        if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("jmri.test.no-dialogs")) {
212            StringBuilder sb = new StringBuilder("<html>");
213            messages.forEach(msg -> {
214                sb.append(msg);
215                sb.append("<br>");
216            });
217            if (helpKey != null) {
218                sb.append("<br>");
219                sb.append(Bundle.getMessage(helpKey));
220            }
221            sb.append("/<html>");
222            JmriJOptionPane.showMessageDialog(null,
223                    sb.toString(),
224                    Bundle.getMessage(titleKey),
225                    JmriJOptionPane.WARNING_MESSAGE);
226        }
227    }
228
229    private void checkItemsHaveParents(SortedSet<? extends MaleSocket> set, List<MaleSocket> beansWithoutParentList) {
230        for (MaleSocket bean : set) {
231            if (bean.getParent() == null) {
232                beansWithoutParentList.add(bean);
233            }
234        }
235    }
236
237    private void checkItemsHaveParents() {
238        List<MaleSocket> beansWithoutParentList = new ArrayList<>();
239        checkItemsHaveParents(InstanceManager.getDefault(AnalogActionManager.class).getNamedBeanSet(), beansWithoutParentList);
240        checkItemsHaveParents(InstanceManager.getDefault(DigitalActionManager.class).getNamedBeanSet(), beansWithoutParentList);
241        checkItemsHaveParents(InstanceManager.getDefault(DigitalBooleanActionManager.class).getNamedBeanSet(), beansWithoutParentList);
242        checkItemsHaveParents(InstanceManager.getDefault(StringActionManager.class).getNamedBeanSet(), beansWithoutParentList);
243        checkItemsHaveParents(InstanceManager.getDefault(AnalogExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
244        checkItemsHaveParents(InstanceManager.getDefault(DigitalExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
245        checkItemsHaveParents(InstanceManager.getDefault(StringExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
246
247        if (!beansWithoutParentList.isEmpty()) {
248            List<String> errors = new ArrayList<>();
249            List<String> msgs = new ArrayList<>();
250            for (Base b : beansWithoutParentList) {
251                b.setup();
252                b.setParentForAllChildren(errors);
253            }
254            for (Base b : beansWithoutParentList) {
255                if (b.getParent() == null) {
256                    log.error("Item has no parent: {}, {}, {}",
257                            b.getSystemName(),
258                            b.getUserName(),
259                            b.getLongDescription());
260                    msgs.add(Bundle.getMessage("NoParentMessage",
261                            b.getSystemName(),
262                            b.getUserName(),
263                            b.getLongDescription()));
264
265                    for (int i=0; i < b.getChildCount(); i++) {
266                        if (b.getChild(i).isConnected()) {
267                            log.error("    Child: {}, {}, {}",
268                                    b.getChild(i).getConnectedSocket().getSystemName(),
269                                    b.getChild(i).getConnectedSocket().getUserName(),
270                                    b.getChild(i).getConnectedSocket().getLongDescription());
271                        }
272                    }
273                    log.error("                                             End Item");
274                    List<String> cliperrors = new ArrayList<String>();
275                    _clipboard.add((MaleSocket) b, cliperrors);
276                }
277            }
278            messageDialog("ParentErrorsTitle", msgs, "NoParentHelp");
279        }
280    }
281
282    /** {@inheritDoc} */
283    @Override
284    public void activateAllLogixNGs() {
285        activateAllLogixNGs(true, true);
286    }
287
288    /** {@inheritDoc} */
289    @Override
290    public void activateAllLogixNGs(boolean runDelayed, boolean runOnSeparateThread) {
291
292        _isActive = true;
293
294        if (_loadDisabled) {
295            for (LogixNG logixNG : _tsys.values()) {
296                logixNG.setEnabled(false);
297            }
298            _loadDisabled = false;
299        }
300
301        // This may take a long time so it must not be done on the GUI thread.
302        // Therefore we create a new thread for this task.
303        Runnable runnable = () -> {
304
305            // Initialize the values of the global variables
306            Set<GlobalVariable> globalVariables =
307                    InstanceManager.getDefault(GlobalVariableManager.class)
308                            .getNamedBeanSet();
309
310            for (GlobalVariable gv : globalVariables) {
311                try {
312                    gv.initialize();
313                } catch (JmriException | IllegalArgumentException e) {
314                    log.warn("Variable {} could not be initialized", gv.getUserName(), e);
315                }
316            }
317
318            Set<LogixNG> activeLogixNGs = new HashSet<>();
319
320            // Activate and execute the initialization LogixNGs first.
321            List<LogixNG> initLogixNGs =
322                    InstanceManager.getDefault(LogixNG_InitializationManager.class)
323                            .getList();
324
325            for (LogixNG logixNG : initLogixNGs) {
326                logixNG.activate();
327                if (logixNG.isActive()) {
328                    logixNG.registerListeners();
329                    logixNG.execute(false, true);
330                    activeLogixNGs.add(logixNG);
331                } else {
332                    logixNG.unregisterListeners();
333                }
334            }
335
336            // Activate and execute all the rest of the LogixNGs.
337            _tsys.values().stream()
338                    .sorted()
339                    .filter((logixNG) -> !(activeLogixNGs.contains(logixNG)))
340                    .forEachOrdered((logixNG) -> {
341
342                logixNG.activate();
343
344                if (logixNG.isActive()) {
345                    logixNG.registerListeners();
346                    logixNG.execute(true, true);
347                } else {
348                    logixNG.unregisterListeners();
349                }
350            });
351
352            // Clear the startup flag of the LogixNGs.
353            _tsys.values().stream().forEach((logixNG) -> {
354                logixNG.clearStartup();
355            });
356        };
357
358        if (runOnSeparateThread) new Thread(runnable).start();
359        else runnable.run();
360    }
361
362    /** {@inheritDoc} */
363    @Override
364    public void deActivateAllLogixNGs() {
365        for (LogixNG logixNG : _tsys.values()) {
366            logixNG.unregisterListeners();
367        }
368        _isActive = false;
369    }
370
371    /** {@inheritDoc} */
372    @Override
373    public boolean isActive() {
374        return _isActive;
375    }
376
377    /** {@inheritDoc} */
378    @Override
379    public void deleteLogixNG(LogixNG x) {
380        // delete the LogixNG
381        deregister(x);
382        x.dispose();
383    }
384
385    /** {@inheritDoc} */
386    @Override
387    public void printTree(
388            PrintTreeSettings settings,
389            PrintWriter writer,
390            String indent,
391            MutableInt lineNumber) {
392
393        printTree(settings, Locale.getDefault(), writer, indent, lineNumber);
394    }
395
396    /** {@inheritDoc} */
397    @Override
398    public void printTree(
399            PrintTreeSettings settings,
400            Locale locale,
401            PrintWriter writer,
402            String indent,
403            MutableInt lineNumber) {
404
405        for (LogixNG logixNG : getNamedBeanSet()) {
406            if (logixNG.isInline()) continue;
407            logixNG.printTree(settings, locale, writer, indent, "", lineNumber);
408            writer.println();
409        }
410
411        for (LogixNG logixNG : getNamedBeanSet()) {
412            if (!logixNG.isInline()) continue;
413            logixNG.printTree(settings, locale, writer, indent, "", lineNumber);
414            writer.println();
415        }
416        InstanceManager.getDefault(ModuleManager.class).printTree(settings, locale, writer, indent, lineNumber);
417        InstanceManager.getDefault(NamedTableManager.class).printTree(locale, writer, indent);
418        InstanceManager.getDefault(GlobalVariableManager.class).printTree(locale, writer, indent);
419        InstanceManager.getDefault(LogixNG_InitializationManager.class).printTree(locale, writer, indent);
420    }
421
422
423    static volatile DefaultLogixNGManager _instance = null;
424
425    @InvokeOnGuiThread  // this method is not thread safe
426    static public DefaultLogixNGManager instance() {
427        if (!ThreadingUtil.isGUIThread()) {
428            LoggingUtil.warnOnce(log, "instance() called on wrong thread");
429        }
430
431        if (_instance == null) {
432            _instance = new DefaultLogixNGManager();
433        }
434        return (_instance);
435    }
436
437    /** {@inheritDoc} */
438    @Override
439    public Class<LogixNG> getNamedBeanClass() {
440        return LogixNG.class;
441    }
442
443    /** {@inheritDoc} */
444    @Override
445    public Clipboard getClipboard() {
446        return _clipboard;
447    }
448
449    /** {@inheritDoc} */
450    @Override
451    public void registerManager(Manager<? extends MaleSocket> manager) {
452        _managers.put(manager.getClass().getName(), manager);
453    }
454
455    /** {@inheritDoc} */
456    @Override
457    public Manager<? extends MaleSocket> getManager(String className) {
458        return _managers.get(className);
459    }
460
461    /**
462     * Inform all registered listeners of a vetoable change.If the propertyName
463     * is "CanDelete" ALL listeners with an interest in the bean will throw an
464     * exception, which is recorded returned back to the invoking method, so
465     * that it can be presented back to the user.However if a listener decides
466     * that the bean can not be deleted then it should throw an exception with
467     * a property name of "DoNotDelete", this is thrown back up to the user and
468     * the delete process should be aborted.
469     *
470     * @param p   The programmatic name of the property that is to be changed.
471     *            "CanDelete" will inquire with all listeners if the item can
472     *            be deleted. "DoDelete" tells the listener to delete the item.
473     * @param old The old value of the property.
474     * @throws java.beans.PropertyVetoException If the recipients wishes the
475     *                                          delete to be aborted (see above)
476     */
477    @OverridingMethodsMustInvokeSuper
478    public void fireVetoableChange(String p, Object old) throws PropertyVetoException {
479        PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, null);
480        for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) {
481            vc.vetoableChange(evt);
482        }
483    }
484
485    /** {@inheritDoc} */
486    @Override
487//    @OverridingMethodsMustInvokeSuper
488    public final void deleteBean(@Nonnull LogixNG logixNG, @Nonnull String property) throws PropertyVetoException {
489        for (int i=logixNG.getNumConditionalNGs()-1; i >= 0; i--) {
490            ConditionalNG child = logixNG.getConditionalNG(i);
491            InstanceManager.getDefault(ConditionalNG_Manager.class).deleteBean(child, property);
492        }
493
494        // throws PropertyVetoException if vetoed
495        fireVetoableChange(property, logixNG);
496        if (property.equals("DoDelete")) { // NOI18N
497            deregister(logixNG);
498            logixNG.dispose();
499        }
500    }
501
502    /** {@inheritDoc} */
503    @Override
504    public void registerSetupTask(Runnable task) {
505        _setupTasks.add(task);
506    }
507
508    /** {@inheritDoc} */
509    @Override
510    public void executeModule(Module module, Object parameter)
511            throws IllegalArgumentException {
512
513        if (module == null) {
514            throw new IllegalArgumentException("The parameter \"module\" is null");
515        }
516        // Get the parameters for the module
517        Collection<Module.Parameter> parameterNames = module.getParameters();
518
519        // Ensure that there is only one parameter
520        if (parameterNames.size() != 1) {
521            throw new IllegalArgumentException("The module doesn't take exactly one parameter");
522        }
523
524        // Get the parameter
525        Module.Parameter param = parameterNames.toArray(Module.Parameter[]::new)[0];
526        if (!param.isInput()) {
527            throw new IllegalArgumentException("The module's parameter is not an input parameter");
528        }
529
530        // Set the value of the parameter
531        Map<String, Object> parameters = new HashMap<>();
532        parameters.put(param.getName(), parameter);
533
534        // Execute the module
535        executeModule(module, parameters);
536    }
537
538    /** {@inheritDoc} */
539    @Override
540    public void executeModule(Module module, Map<String, Object> parameters)
541            throws IllegalArgumentException {
542        DefaultConditionalNG.executeModule(module, parameters);
543    }
544
545    /** {@inheritDoc} */
546    @Override
547    public FemaleSocket getErrorHandlingModuleSocket() {
548        return AbstractMaleSocket.getErrorHandlingModuleSocket();
549    }
550
551    /** {@inheritDoc} */
552    @Override
553    public boolean isErrorHandlingModuleEnabled() {
554        return AbstractMaleSocket.isErrorHandlingModuleEnabled();
555    }
556
557    /**
558     * The PropertyChangeListener interface in this class is intended to keep
559     * track of user name changes to individual NamedBeans. It is not completely
560     * implemented yet. In particular, listeners are not added to newly
561     * registered objects.
562     *
563     * @param e the event
564     */
565    @Override
566    @OverridingMethodsMustInvokeSuper
567    public void propertyChange(PropertyChangeEvent e) {
568        super.propertyChange(e);
569        if (LogixNG.PROPERTY_INLINE.equals(e.getPropertyName())) {
570            // If a LogixNG changes its "inline" state, the number of items
571            // listed in the LogixNG table might change.
572            firePropertyChange("length", null, _beans.size());
573        }
574    }
575
576
577    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultLogixNGManager.class);
578
579}