001package jmri.managers.configurexml;
002
003import java.awt.GraphicsEnvironment;
004import java.util.List;
005import java.util.SortedSet;
006
007import jmri.InstanceManager;
008import jmri.Route;
009import jmri.RouteManager;
010import jmri.Sensor;
011import jmri.Turnout;
012import jmri.managers.DefaultRouteManager;
013import jmri.util.swing.JmriJOptionPane;
014
015import org.jdom2.Element;
016
017/**
018 * Provides the functionality for configuring RouteManagers.
019 *
020 * @author Dave Duchamp Copyright (c) 2004
021 * @author Daniel Boudreau Copyright (c) 2007
022 * @author Simon Reader Copyright (C) 2008
023 */
024public class DefaultRouteManagerXml extends jmri.managers.configurexml.AbstractNamedBeanManagerConfigXML {
025
026    public DefaultRouteManagerXml() {
027    }
028
029    /**
030     * Default implementation for storing the contents of a RouteManager.
031     *
032     * @param o Object to store, of type RouteManager
033     * @return Element containing the complete info
034     */
035    @Override
036    public Element store(Object o) {
037        Element routes = new Element("routes");
038        setStoreElementClass(routes);
039        RouteManager rm = (RouteManager) o;
040        if (rm != null) {
041            SortedSet<Route> routeList = rm.getNamedBeanSet();
042            // don't return an element if there are no routes to include
043            if (routeList.isEmpty()) {
044                return null;
045            }
046            for (Route r : routeList) {
047                // store the routes
048                String rName = r.getSystemName();
049                log.debug("system name is {}", rName);
050
051                String cTurnout = r.getControlTurnout();
052                int addedDelay = r.getRouteCommandDelay();
053                boolean routeLocked = r.getLocked();
054                String cLockTurnout = r.getLockControlTurnout();
055
056                Element elem = new Element("route");
057                elem.addContent(new Element("systemName").addContent(rName));
058
059                // As a work-around for backward compatibility, store systemName and userName as attribute.
060                // TODO Remove this in e.g. JMRI 4.11.1 and then update all the loadref comparison files
061                String uName = r.getUserName();
062                if (uName != null && !uName.equals("")) {
063                    elem.setAttribute("userName", uName);
064                }
065
066                // store common parts
067                storeCommon(r, elem);
068
069                if (cTurnout != null && !cTurnout.equals("")) {
070                    elem.setAttribute("controlTurnout", cTurnout);
071                    int state = r.getControlTurnoutState();
072                    if (state == Route.ONTHROWN) {
073                        elem.setAttribute("controlTurnoutState", "THROWN");
074                    } else if (state == Route.ONCHANGE) {
075                        elem.setAttribute("controlTurnoutState", "CHANGE");
076                    } else if (state == Route.VETOCLOSED) {
077                        elem.setAttribute("controlTurnoutState", "VETOCLOSED");
078                    } else if (state == Route.VETOTHROWN) {
079                        elem.setAttribute("controlTurnoutState", "VETOTHROWN");
080                    } else {
081                        elem.setAttribute("controlTurnoutState", "CLOSED");
082                    }
083                    
084                    if (r.getControlTurnoutFeedback()) {
085                        elem.setAttribute("controlTurnoutFeedback", "true");
086                    } // don't write if not set, accept default
087                }
088                if (cLockTurnout != null && !cLockTurnout.equals("")) {
089                    elem.setAttribute("controlLockTurnout", cLockTurnout);
090                    int state = r.getLockControlTurnoutState();
091                    if (state == Route.ONTHROWN) {
092                        elem.setAttribute("controlLockTurnoutState", "THROWN");
093                    } else if (state == Route.ONCHANGE) {
094                        elem.setAttribute("controlLockTurnoutState", "CHANGE");
095                    } else {
096                        elem.setAttribute("controlLockTurnoutState", "CLOSED");
097                    }
098                }
099                if (addedDelay > 0) {
100                    elem.setAttribute("addedDelay", Integer.toString(addedDelay));
101                }
102
103                if (routeLocked) {
104                    elem.setAttribute("routeLocked", "True");
105                }
106                // add route output Turnouts, if any
107                int index = 0;
108                String rTurnout = null;
109                while ((rTurnout = r.getOutputTurnoutByIndex(index)) != null) {
110                    Element rElem = new Element("routeOutputTurnout")
111                            .setAttribute("systemName", rTurnout);
112                    String sState = "CLOSED";
113                    if (r.getOutputTurnoutSetState(rTurnout) == Turnout.THROWN) {
114                        sState = "THROWN";
115                    } else if (r.getOutputTurnoutSetState(rTurnout) == Route.TOGGLE) {
116                        sState = "TOGGLE";
117                    }
118                    rElem.setAttribute("state", sState);
119                    elem.addContent(rElem);
120                    index++;
121                }
122                // add route output Sensors, if any
123                index = 0;
124                String rSensor = null;
125                while ((rSensor = r.getOutputSensorByIndex(index)) != null) {
126                    Element rElem = new Element("routeOutputSensor")
127                            .setAttribute("systemName", rSensor);
128                    String sState = "INACTIVE";
129                    if (r.getOutputSensorSetState(rSensor) == Sensor.ACTIVE) {
130                        sState = "ACTIVE";
131                    } else if (r.getOutputSensorSetState(rSensor) == Route.TOGGLE) {
132                        sState = "TOGGLE";
133                    }
134                    rElem.setAttribute("state", sState);
135                    elem.addContent(rElem);
136                    index++;
137                }
138                // add route control Sensors, if any
139                index = 0;
140                while ((rSensor = r.getRouteSensorName(index)) != null) {
141                    Element rsElem = new Element("routeSensor")
142                            .setAttribute("systemName", rSensor);
143                    int mode = r.getRouteSensorMode(index);
144                    String modeName;
145                    switch (mode) {
146                        case Route.ONACTIVE:
147                            modeName = "onActive";
148                            break;
149                        case Route.ONINACTIVE:
150                            modeName = "onInactive";
151                            break;
152                        case Route.ONCHANGE:
153                            modeName = "onChange";
154                            break;
155                        case Route.VETOACTIVE:
156                            modeName = "vetoActive";
157                            break;
158                        case Route.VETOINACTIVE:
159                            modeName = "vetoInactive";
160                            break;
161                        default:
162                            modeName = null;
163                    }
164                    if (modeName != null) {
165                        rsElem.setAttribute("mode", modeName);
166                    }
167                    elem.addContent(rsElem);
168                    index++;
169                }
170                // add sound and script file elements if needed
171                if (r.getOutputSoundName() != null && !r.getOutputSoundName().equals("")) {
172                    Element rsElem = new Element("routeSoundFile")
173                            .setAttribute("name",
174                                    jmri.util.FileUtil.getPortableFilename(
175                                            new java.io.File(r.getOutputSoundName()))
176                            );
177                    elem.addContent(rsElem);
178                }
179                if (r.getOutputScriptName() != null && !r.getOutputScriptName().equals("")) {
180                    Element rsElem = new Element("routeScriptFile")
181                            .setAttribute("name",
182                                    jmri.util.FileUtil.getPortableFilename(
183                                            new java.io.File(r.getOutputScriptName()))
184                            );
185                    elem.addContent(rsElem);
186                }
187
188                // add turnouts aligned sensor if there is one
189                if (!r.getTurnoutsAlignedSensor().equals("")) {
190                    Element rsElem = new Element("turnoutsAlignedSensor")
191                            .setAttribute("name", r.getTurnoutsAlignedSensor());
192                    elem.addContent(rsElem);
193                }
194
195                log.debug("store Route {}", rName);
196                routes.addContent(elem);
197            }
198        }
199        return routes;
200    }
201
202    /**
203     * Subclass provides implementation to create the correct top element,
204     * including the type information. Default implementation is to use the
205     * local class here.
206     *
207     * @param routes The top-level element being created
208     */
209    public void setStoreElementClass(Element routes) {
210        routes.setAttribute("class", this.getClass().getName());
211    }
212
213    /**
214     * Create a RouteManager object of the correct class, then register and fill
215     * it.
216     *
217     * @param sharedRoutes Top level Element to unpack.
218     * @return true if successful
219     */
220    @Override
221    public boolean load(Element sharedRoutes, Element perNodeRoutes) {
222        // create the master object
223        replaceRouteManager();
224        // load individual sharedRoutes
225        loadRoutes(sharedRoutes);
226        return true;
227    }
228
229    /**
230     * Utility method to load the individual Route objects. If there's no
231     * additional info needed for a specific route type, invoke this with the
232     * parent of the set of Route elements.
233     *
234     * @param routes Element containing the Route elements to load.
235     */
236    public void loadRoutes(Element routes) {
237        List<Element> routeList = routes.getChildren("route");
238        log.debug("Found {} routes", routeList.size());
239        RouteManager tm = InstanceManager.getDefault(jmri.RouteManager.class);
240        int namesChanged = 0;
241
242        for (Element el : routeList) {
243
244            String sysName = getSystemName(el);
245            if (sysName == null) {
246                log.warn("unexpected null in systemName {}", el);
247                break;
248            }
249            // convert typeLetter from R to tm.typeLetter()
250            if (sysName.startsWith(tm.getSystemPrefix() + 'R')) {
251                String old = sysName;
252                sysName = tm.getSystemNamePrefix() + sysName.substring(tm.getSystemNamePrefix().length());
253                log.warn("Converting route system name {} to {}", old, sysName);
254                namesChanged++;
255            }
256            // prepend systemNamePrefix if missing
257            if (!sysName.startsWith(tm.getSystemNamePrefix())) {
258                String old = sysName;
259                sysName = tm.getSystemNamePrefix() + sysName;
260                log.warn("Converting route system name {} to {}", old, sysName);
261                namesChanged++;
262            }
263
264            String userName = getUserName(el);
265            String cTurnout = null;
266            String cTurnoutState = null;
267            boolean cTurnoutFeedback = false;
268            String addedDelayTxt = null;
269            String routeLockedTxt = null;
270            String cLockTurnout = null;
271            String cLockTurnoutState = null;
272            int addedDelay = 0;
273
274            if (el.getAttribute("controlTurnout") != null) {
275                cTurnout = el.getAttribute("controlTurnout").getValue();
276            }
277            if (el.getAttribute("controlTurnoutState") != null) {
278                cTurnoutState = el.getAttribute("controlTurnoutState").getValue();
279            }
280            if (el.getAttribute("controlTurnoutFeedback") != null) {
281                cTurnoutFeedback = el.getAttribute("controlTurnoutFeedback").getValue().equals("true");
282            }
283            if (el.getAttribute("controlLockTurnout") != null) {
284                cLockTurnout = el.getAttribute("controlLockTurnout").getValue();
285            }
286            if (el.getAttribute("controlLockTurnoutState") != null) {
287                cLockTurnoutState = el.getAttribute("controlLockTurnoutState").getValue();
288            }
289            if (el.getAttribute("addedDelay") != null) {
290                addedDelayTxt = el.getAttribute("addedDelay").getValue();
291                if (addedDelayTxt != null) {
292                    addedDelay = Integer.parseInt(addedDelayTxt);
293                }
294            }
295            if (el.getAttribute("routeLocked") != null) {
296                routeLockedTxt = el.getAttribute("routeLocked").getValue();
297            }
298
299            log.debug("create route: ({})({})", sysName, (userName == null ? "<null>" : userName));
300            
301            Route r;
302            try {
303                r = tm.provideRoute(sysName, userName);
304            } catch (IllegalArgumentException ex) {
305                log.error("failed to create Route: {}", sysName);
306                return;
307            }
308
309            // load common parts
310            loadCommon(r, el);
311
312            // add control turnout if there is one
313            if (cTurnout != null) {
314                r.setControlTurnout(cTurnout);
315                if (cTurnoutState != null) {
316                    switch (cTurnoutState) {
317                        case "THROWN":
318                            r.setControlTurnoutState(Route.ONTHROWN);
319                            break;
320                        case "CHANGE":
321                            r.setControlTurnoutState(Route.ONCHANGE);
322                            break;
323                        case "VETOCLOSED":
324                            r.setControlTurnoutState(Route.VETOCLOSED);
325                            break;
326                        case "VETOTHROWN":
327                            r.setControlTurnoutState(Route.VETOTHROWN);
328                            break;
329                        default:
330                            r.setControlTurnoutState(Route.ONCLOSED);
331                    }
332                } else {
333                    log.error("cTurnoutState was null!");
334                }
335                r.setControlTurnoutFeedback(cTurnoutFeedback);
336            }
337            // set added delay
338            r.setRouteCommandDelay(addedDelay);
339
340            // determine if route locked
341            if (routeLockedTxt != null && routeLockedTxt.equals("True")) {
342                r.setLocked(true);
343            }
344
345            // add lock control turout if there is one
346            if (cLockTurnout != null) {
347                r.setLockControlTurnout(cLockTurnout);
348                if (cLockTurnoutState != null) {
349                    if (cLockTurnoutState.equals("THROWN")) {
350                        r.setLockControlTurnoutState(Route.ONTHROWN);
351                    } else if (cLockTurnoutState.equals("CHANGE")) {
352                        r.setLockControlTurnoutState(Route.ONCHANGE);
353                    } else {
354                        r.setLockControlTurnoutState(Route.ONCLOSED);
355                    }
356                } else {
357                    log.error("cLockTurnoutState was null!");
358                }
359            }
360
361            // load output turnouts if there are any - old format first (1.7.6 and before)
362            List<Element> routeTurnoutList = el.getChildren("routeTurnout");
363            if (routeTurnoutList.size() > 0) {
364                // This route has turnouts
365                for (Element element : routeTurnoutList) {
366                    if (element.getAttribute("systemName") == null) {
367                        log.warn("unexpected null in systemName {} {}", element, element.getAttributes());
368                        break;
369                    }
370                    String tSysName = element.getAttribute("systemName").getValue();
371                    String rState = element.getAttribute("state").getValue();
372                    int tSetState = Turnout.CLOSED;
373                    if (rState.equals("THROWN")) {
374                        tSetState = Turnout.THROWN;
375                    } else if (rState.equals("TOGGLE")) {
376                        tSetState = Route.TOGGLE;
377                    }
378                    // Add turnout to route
379                    r.addOutputTurnout(tSysName, tSetState);
380                }
381            }
382            // load output turnouts if there are any - new format
383            routeTurnoutList = el.getChildren("routeOutputTurnout");
384            if (routeTurnoutList.size() > 0) {
385                // This route has turnouts
386                for (int k = 0; k < routeTurnoutList.size(); k++) { // index k is required later to get Locked state
387                    if (routeTurnoutList.get(k).getAttribute("systemName") == null) {
388                        log.warn("unexpected null in systemName {} {}", routeTurnoutList.get(k),
389                                routeTurnoutList.get(k).getAttributes());
390                        break;
391                    }
392                    String tSysName = routeTurnoutList.get(k)
393                            .getAttribute("systemName").getValue();
394                    String rState = routeTurnoutList.get(k)
395                            .getAttribute("state").getValue();
396                    int tSetState = Turnout.CLOSED;
397                    if (rState.equals("THROWN")) {
398                        tSetState = Turnout.THROWN;
399                    } else if (rState.equals("TOGGLE")) {
400                        tSetState = Route.TOGGLE;
401                    }
402                    // If the Turnout has already been added to the route and is the same as that loaded, 
403                    // we will not re add the turnout.
404                    if (!r.isOutputTurnoutIncluded(tSysName)) {
405
406                        // Add turnout to route
407                        r.addOutputTurnout(tSysName, tSetState);
408
409                        // determine if turnout should be locked
410                        Turnout t = r.getOutputTurnout(k);
411                        if (r.getLocked()) {
412                            t.setLocked(Turnout.CABLOCKOUT + Turnout.PUSHBUTTONLOCKOUT, true);
413                        }
414                    }
415                }
416            }
417            // load output sensors if there are any - new format
418            List<Element> routeSensorList = el.getChildren("routeOutputSensor");
419            for (Element sen : routeSensorList) { // this route has output sensors
420                if (sen.getAttribute("systemName") == null) {
421                    log.warn("unexpected null in systemName {} {}", sen, sen.getAttributes());
422                    break;
423                }
424                String tSysName = sen.getAttribute("systemName").getValue();
425                String rState = sen.getAttribute("state").getValue();
426                int tSetState = Sensor.INACTIVE;
427                if (rState.equals("ACTIVE")) {
428                    tSetState = Sensor.ACTIVE;
429                } else if (rState.equals("TOGGLE")) {
430                    tSetState = Route.TOGGLE;
431                }
432                // If the Turnout has already been added to the route and is the same as that loaded,
433                // we will not re add the turnout.
434                if (r.isOutputSensorIncluded(tSysName)) {
435                    break;
436                }
437                // Add turnout to route
438                r.addOutputSensor(tSysName, tSetState);
439            }
440            // load sound, script files if present
441            Element fileElement = el.getChild("routeSoundFile");
442            if (fileElement != null) {
443                // convert to absolute path name
444                r.setOutputSoundName(
445                        jmri.util.FileUtil.getExternalFilename(fileElement.getAttribute("name").getValue())
446                );
447            }
448            fileElement = el.getChild("routeScriptFile");
449            if (fileElement != null) {
450                r.setOutputScriptName(
451                        jmri.util.FileUtil.getExternalFilename(fileElement.getAttribute("name").getValue())
452                );
453            }
454            // load turnouts aligned sensor if there is one
455            fileElement = el.getChild("turnoutsAlignedSensor");
456            if (fileElement != null) {
457                r.setTurnoutsAlignedSensor(fileElement.getAttribute("name").getValue());
458            }
459
460            // load route control sensors, if there are any
461            routeSensorList = el.getChildren("routeSensor");
462            for (Element sen : routeSensorList) { // this route has sensors
463                if (sen.getAttribute("systemName") == null) {
464                    log.warn("unexpected null in systemName {} {}", sen, sen.getAttributes());
465                    break;
466                }
467                int mode = Route.ONACTIVE;  // default mode
468                if (sen.getAttribute("mode") == null) {
469                    break;
470                }
471                String sm = sen.getAttribute("mode").getValue();
472                switch (sm) {
473                    case "onActive":
474                        mode = Route.ONACTIVE;
475                        break;
476                    case "onInactive":
477                        mode = Route.ONINACTIVE;
478                        break;
479                    case "onChange":
480                        mode = Route.ONCHANGE;
481                        break;
482                    case "vetoActive":
483                        mode = Route.VETOACTIVE;
484                        break;
485                    case "vetoInactive":
486                        mode = Route.VETOINACTIVE;
487                        break;
488                    default:
489                        log.warn("unexpected sensor mode in route {} was {}", sysName, sm);
490                }
491                // Add Sensor to route
492                r.addSensorToRoute(sen.getAttribute("systemName").getValue(), mode);
493            }
494            // and start it working
495            r.activateRoute();
496        }
497        if (namesChanged > 0) {
498            // TODO: replace the System property check with an in-application mechanism
499            // for notifying users of multiple changes that can be silenced as part of
500            // normal operations
501            if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("jmri.test.no-dialogs")) {
502                JmriJOptionPane.showMessageDialog(null,
503                        Bundle.getMessage(namesChanged > 1 ? "RouteManager.SystemNamesChanged.Message" : "RouteManager.SystemNameChanged.Message", namesChanged),
504                        Bundle.getMessage("Manager.SystemNamesChanged.Title", namesChanged, tm.getBeanTypeHandled(namesChanged > 1)),
505                        JmriJOptionPane.WARNING_MESSAGE);
506            }
507            log.warn("System names for {} Routes changed; this may have operational impacts.", namesChanged); 
508        }
509    }
510
511    /**
512     * Replace the current RouteManager, if there is one, with one newly created
513     * during a load operation. This is skipped if the present one is already of
514     * the right type.
515     */
516    protected void replaceRouteManager() {
517        RouteManager current = InstanceManager.getNullableDefault(jmri.RouteManager.class);
518        if (current != null && current.getClass().getName()
519                .equals(DefaultRouteManager.class.getName())) {
520            return;
521        }
522        // if old manager exists, remove it from configuration process
523        if (current != null) {
524            InstanceManager.getDefault(jmri.ConfigureManager.class).deregister(current);
525            InstanceManager.deregister(current, RouteManager.class);
526        }
527
528        // register new one with InstanceManager
529        DefaultRouteManager pManager = InstanceManager.getDefault(DefaultRouteManager.class);
530        InstanceManager.store(pManager, RouteManager.class);
531        // register new one for configuration
532        InstanceManager.getDefault(jmri.ConfigureManager.class).registerConfig(pManager, jmri.Manager.ROUTES);
533    }
534
535    @Override
536    public int loadOrder() {
537        return InstanceManager.getDefault(jmri.RouteManager.class).getXMLOrder();
538    }
539
540    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultRouteManagerXml.class);
541
542}