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 (((Base)bean).getParent() == null) beansWithoutParentList.add(bean);
229        }
230    }
231
232    private void checkItemsHaveParents() {
233        List<MaleSocket> beansWithoutParentList = new ArrayList<>();
234        checkItemsHaveParents(InstanceManager.getDefault(AnalogActionManager.class).getNamedBeanSet(), beansWithoutParentList);
235        checkItemsHaveParents(InstanceManager.getDefault(DigitalActionManager.class).getNamedBeanSet(), beansWithoutParentList);
236        checkItemsHaveParents(InstanceManager.getDefault(DigitalBooleanActionManager.class).getNamedBeanSet(), beansWithoutParentList);
237        checkItemsHaveParents(InstanceManager.getDefault(StringActionManager.class).getNamedBeanSet(), beansWithoutParentList);
238        checkItemsHaveParents(InstanceManager.getDefault(AnalogExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
239        checkItemsHaveParents(InstanceManager.getDefault(DigitalExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
240        checkItemsHaveParents(InstanceManager.getDefault(StringExpressionManager.class).getNamedBeanSet(), beansWithoutParentList);
241
242        if (!beansWithoutParentList.isEmpty()) {
243            List<String> errors = new ArrayList<>();
244            List<String> msgs = new ArrayList<>();
245            for (Base b : beansWithoutParentList) {
246                b.setup();
247                b.setParentForAllChildren(errors);
248            }
249            for (Base b : beansWithoutParentList) {
250                if (b.getParent() == null) {
251                    log.error("Item has no parent: {}, {}, {}",
252                            b.getSystemName(),
253                            b.getUserName(),
254                            b.getLongDescription());
255                    msgs.add(Bundle.getMessage("NoParentMessage",
256                            b.getSystemName(),
257                            b.getUserName(),
258                            b.getLongDescription()));
259
260                    for (int i=0; i < b.getChildCount(); i++) {
261                        if (b.getChild(i).isConnected()) {
262                            log.error("    Child: {}, {}, {}",
263                                    b.getChild(i).getConnectedSocket().getSystemName(),
264                                    b.getChild(i).getConnectedSocket().getUserName(),
265                                    b.getChild(i).getConnectedSocket().getLongDescription());
266                        }
267                    }
268                    log.error("                                             End Item");
269                    List<String> cliperrors = new ArrayList<String>();
270                    _clipboard.add((MaleSocket) b, cliperrors);
271                }
272            }
273            messageDialog("ParentErrorsTitle", msgs, "NoParentHelp");
274        }
275    }
276
277    /** {@inheritDoc} */
278    @Override
279    public void activateAllLogixNGs() {
280        activateAllLogixNGs(true, true);
281    }
282
283    /** {@inheritDoc} */
284    @Override
285    public void activateAllLogixNGs(boolean runDelayed, boolean runOnSeparateThread) {
286
287        _isActive = true;
288
289        if (_loadDisabled) {
290            for (LogixNG logixNG : _tsys.values()) {
291                logixNG.setEnabled(false);
292            }
293            _loadDisabled = false;
294        }
295
296        // This may take a long time so it must not be done on the GUI thread.
297        // Therefore we create a new thread for this task.
298        Runnable runnable = () -> {
299
300            // Initialize the values of the global variables
301            Set<GlobalVariable> globalVariables =
302                    InstanceManager.getDefault(GlobalVariableManager.class)
303                            .getNamedBeanSet();
304
305            for (GlobalVariable gv : globalVariables) {
306                try {
307                    gv.initialize();
308                } catch (JmriException | IllegalArgumentException e) {
309                    log.warn("Variable {} could not be initialized", gv.getUserName(), e);
310                }
311            }
312
313            Set<LogixNG> activeLogixNGs = new HashSet<>();
314
315            // Activate and execute the initialization LogixNGs first.
316            List<LogixNG> initLogixNGs =
317                    InstanceManager.getDefault(LogixNG_InitializationManager.class)
318                            .getList();
319
320            for (LogixNG logixNG : initLogixNGs) {
321                logixNG.activate();
322                if (logixNG.isActive()) {
323                    logixNG.registerListeners();
324                    logixNG.execute(false, true);
325                    activeLogixNGs.add(logixNG);
326                } else {
327                    logixNG.unregisterListeners();
328                }
329            }
330
331            // Activate and execute all the rest of the LogixNGs.
332            _tsys.values().stream()
333                    .sorted()
334                    .filter((logixNG) -> !(activeLogixNGs.contains(logixNG)))
335                    .forEachOrdered((logixNG) -> {
336
337                logixNG.activate();
338
339                if (logixNG.isActive()) {
340                    logixNG.registerListeners();
341                    logixNG.execute(true, true);
342                } else {
343                    logixNG.unregisterListeners();
344                }
345            });
346
347            // Clear the startup flag of the LogixNGs.
348            _tsys.values().stream().forEach((logixNG) -> {
349                logixNG.clearStartup();
350            });
351        };
352
353        if (runOnSeparateThread) new Thread(runnable).start();
354        else runnable.run();
355    }
356
357    /** {@inheritDoc} */
358    @Override
359    public void deActivateAllLogixNGs() {
360        for (LogixNG logixNG : _tsys.values()) {
361            logixNG.unregisterListeners();
362        }
363        _isActive = false;
364    }
365
366    /** {@inheritDoc} */
367    @Override
368    public boolean isActive() {
369        return _isActive;
370    }
371
372    /** {@inheritDoc} */
373    @Override
374    public void deleteLogixNG(LogixNG x) {
375        // delete the LogixNG
376        deregister(x);
377        x.dispose();
378    }
379
380    /** {@inheritDoc} */
381    @Override
382    public void printTree(
383            PrintTreeSettings settings,
384            PrintWriter writer,
385            String indent,
386            MutableInt lineNumber) {
387
388        printTree(settings, Locale.getDefault(), writer, indent, lineNumber);
389    }
390
391    /** {@inheritDoc} */
392    @Override
393    public void printTree(
394            PrintTreeSettings settings,
395            Locale locale,
396            PrintWriter writer,
397            String indent,
398            MutableInt lineNumber) {
399
400        for (LogixNG logixNG : getNamedBeanSet()) {
401            logixNG.printTree(settings, locale, writer, indent, "", lineNumber);
402            writer.println();
403        }
404        InstanceManager.getDefault(ModuleManager.class).printTree(settings, locale, writer, indent, lineNumber);
405        InstanceManager.getDefault(NamedTableManager.class).printTree(locale, writer, indent);
406        InstanceManager.getDefault(GlobalVariableManager.class).printTree(locale, writer, indent);
407        InstanceManager.getDefault(LogixNG_InitializationManager.class).printTree(locale, writer, indent);
408    }
409
410
411    static volatile DefaultLogixNGManager _instance = null;
412
413    @InvokeOnGuiThread  // this method is not thread safe
414    static public DefaultLogixNGManager instance() {
415        if (!ThreadingUtil.isGUIThread()) {
416            LoggingUtil.warnOnce(log, "instance() called on wrong thread");
417        }
418
419        if (_instance == null) {
420            _instance = new DefaultLogixNGManager();
421        }
422        return (_instance);
423    }
424
425    /** {@inheritDoc} */
426    @Override
427    public Class<LogixNG> getNamedBeanClass() {
428        return LogixNG.class;
429    }
430
431    /** {@inheritDoc} */
432    @Override
433    public Clipboard getClipboard() {
434        return _clipboard;
435    }
436
437    /** {@inheritDoc} */
438    @Override
439    public void registerManager(Manager<? extends MaleSocket> manager) {
440        _managers.put(manager.getClass().getName(), manager);
441    }
442
443    /** {@inheritDoc} */
444    @Override
445    public Manager<? extends MaleSocket> getManager(String className) {
446        return _managers.get(className);
447    }
448
449    /**
450     * Inform all registered listeners of a vetoable change.If the propertyName
451     * is "CanDelete" ALL listeners with an interest in the bean will throw an
452     * exception, which is recorded returned back to the invoking method, so
453     * that it can be presented back to the user.However if a listener decides
454     * that the bean can not be deleted then it should throw an exception with
455     * a property name of "DoNotDelete", this is thrown back up to the user and
456     * the delete process should be aborted.
457     *
458     * @param p   The programmatic name of the property that is to be changed.
459     *            "CanDelete" will inquire with all listeners if the item can
460     *            be deleted. "DoDelete" tells the listener to delete the item.
461     * @param old The old value of the property.
462     * @throws java.beans.PropertyVetoException If the recipients wishes the
463     *                                          delete to be aborted (see above)
464     */
465    @OverridingMethodsMustInvokeSuper
466    public void fireVetoableChange(String p, Object old) throws PropertyVetoException {
467        PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, null);
468        for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) {
469            vc.vetoableChange(evt);
470        }
471    }
472
473    /** {@inheritDoc} */
474    @Override
475//    @OverridingMethodsMustInvokeSuper
476    public final void deleteBean(@Nonnull LogixNG logixNG, @Nonnull String property) throws PropertyVetoException {
477        for (int i=logixNG.getNumConditionalNGs()-1; i >= 0; i--) {
478            ConditionalNG child = logixNG.getConditionalNG(i);
479            InstanceManager.getDefault(ConditionalNG_Manager.class).deleteBean(child, property);
480        }
481
482        // throws PropertyVetoException if vetoed
483        fireVetoableChange(property, logixNG);
484        if (property.equals("DoDelete")) { // NOI18N
485            deregister(logixNG);
486            logixNG.dispose();
487        }
488    }
489
490    /** {@inheritDoc} */
491    @Override
492    public void registerSetupTask(Runnable task) {
493        _setupTasks.add(task);
494    }
495
496    /**
497     * The PropertyChangeListener interface in this class is intended to keep
498     * track of user name changes to individual NamedBeans. It is not completely
499     * implemented yet. In particular, listeners are not added to newly
500     * registered objects.
501     *
502     * @param e the event
503     */
504    @Override
505    @OverridingMethodsMustInvokeSuper
506    public void propertyChange(PropertyChangeEvent e) {
507        super.propertyChange(e);
508        if (LogixNG.PROPERTY_INLINE.equals(e.getPropertyName())) {
509            // If a LogixNG changes its "inline" state, the number of items
510            // listed in the LogixNG table might change.
511            firePropertyChange("length", null, _beans.size());
512        }
513    }
514
515
516    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultLogixNGManager.class);
517
518}