001package jmri.jmrix.loconet.configurexml;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.File;
006import java.io.IOException;
007import java.io.FileNotFoundException;
008
009import jmri.jmrit.XmlFile;
010import jmri.jmrix.loconet.LnConstants;
011import jmri.jmrix.loconet.Ln7gAccyRoutesManager;
012import jmri.jmrix.loconet.alm.LnSimple7thGenDeviceRoutes;
013import jmri.jmrix.loconet.alm.LnSimpleRouteEntry;
014import jmri.jmrix.loconet.alm.RouteSwitchPositionEnum;
015import jmri.util.FileUtil;
016
017import org.jdom2.Document;
018import org.jdom2.Element;
019import org.jdom2.JDOMException;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * Handle XML configuration for sevgen.Accy7thGenRoutes objects.
026 *
027 * @author B. Milhaupt; Copyright(c) 2024
028 */
029
030public class Digitrax7thGenAccyRoutesXML {
031    private final Ln7gAccyRoutesManager l7garm;
032
033    public Digitrax7thGenAccyRoutesXML(Ln7gAccyRoutesManager l7garm) {
034        this.l7garm = l7garm;
035    }
036
037    /**
038     * Load the Routes info from file.
039     * @return true if loaded
040     */
041    public boolean loadXML() {
042        if (l7garm.getCountOfDevicesWithRoutes() > 0) {
043            log.warn("loadXML: already loaded.  Quitting.");
044            return true;
045        }
046        DigitraxRoutesXmlFile xmlFile = new DigitraxRoutesXmlFile();
047        File file = xmlFile.getLoadFile();
048
049        if (file == null) {
050            doStore();
051            file = xmlFile.getStoreFile();
052        }
053
054        if (!file.exists()) {
055            log.warn("loadXML: Note: File does not exist.");
056            return false;
057        }
058
059        // Find digitraxRoutesPreferencesElement
060        Element root;
061        try {
062            root = xmlFile.rootFromFile(file);
063            if (root == null) {
064                log.warn("loadXML: File could not be read");  // NOI18N
065                return false;
066            }
067            if (!root.getName().equals("DigitraxRoutesPreferences") ) {  // NOI18N
068                log.warn("loadXML: Wrong root name: {}",root.getName());
069                return false;
070            }
071
072            for (Element routes: root.getChildren()) {
073                if (!routes.getAttributeValue("class").equals("Accy7thGenRoutes.configurexml.RoutesManagerXml")) {
074                    log.debug("loadXML: routes ignored at element {}, class {}",
075                            routes.getName(), routes.getAttribute("class"));
076                } else {
077                    for (Element device: routes.getChildren("device")) {
078                        String type=device.getAttributeValue("type");
079                        String serNum = device.getAttributeValue("serNum");
080                        String baseAddr = device.getAttributeValue("baseAddr");
081                        log.debug("loadXML: device {}, serNum {}, baseAddr {}",
082                                type, serNum, baseAddr);
083
084                        LnSimple7thGenDeviceRoutes dev =
085                                new LnSimple7thGenDeviceRoutes(
086                                        LnSimple7thGenDeviceRoutes.getDeviceType(type),
087                                        Integer.parseInt(serNum));
088                        dev.setBaseAddr(Integer.parseInt(baseAddr));
089                        log.debug("loadXML: add {}", dev.getDeviceType());
090
091                        int numRoutes = ((dev.getDeviceType() == LnConstants.RE_IPL_DIGITRAX_HOST_DS78V) ? 16:8);
092                        for (int i = 0; i < numRoutes; ++i) {
093                            for (int j =0; j<8; ++j) {
094                                dev.setOneEntry(i, j, -1, RouteSwitchPositionEnum.UNUSED);
095                            }
096                        }
097
098                        for (Element route: device.getChildren()) {
099                            log.debug("\tdevice's route {}", route.getAttributeValue("number"));
100                            int rn = Integer.parseInt(route.getAttributeValue("number")) - 1;
101
102                            int entryNum = 0;
103
104                            for (Element entry: route.getChildren()) {
105                                if (entry.getName().equals("routeTop")) {
106                                    log.debug("\t\trouteTop: {}, {}",
107                                            entry.getAttributeValue("controlTurnout"),
108                                            entry.getAttributeValue("controlTurnoutState"));
109                                    int t = Integer.parseInt(entry.getAttributeValue("controlTurnout"));
110                                    dev.setOneEntry(rn, entryNum, t,
111                                            RouteSwitchPositionEnum.valueOf(entry.getAttributeValue("controlTurnoutState")));
112                                } else {
113                                    log.debug("\t\tsubsequent Entry: {}, {}",
114                                            entry.getAttributeValue("turnout"),
115                                            entry.getAttributeValue("state"));
116                                    dev.setOneEntry(rn, entryNum, Integer.parseInt(entry.getAttributeValue("turnout")),
117                                            RouteSwitchPositionEnum.valueOf(entry.getAttributeValue("state")));
118                                }
119                                entryNum++;
120                            }
121                        }
122                        // Register the device/serNum/BaseAddr and routes
123                        // to the Ln7gAccyRoutesManager
124                        l7garm.addDevice(dev);
125                    }
126                }
127            }
128        } catch (JDOMException ex) {
129            log.error("loadXML: File invalid", ex);  // NOI18N
130            return false;
131        } catch (IOException ex) {
132            log.error("loadXML: Error reading file", ex);  // NOI18N
133            return false;
134        }
135
136        log.debug("loadXML: Finished reading the 'DigitraxRoutes' file.");
137        return false;
138    }
139
140
141    /**
142     * Store the Routes info.
143     * @return true if stored
144     */
145    @SuppressFBWarnings(value="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
146    public boolean doStore()  {
147        // Create digitraxRoutesPreferencesElement element
148        Element digitraxRoutesPreferencesElement = new Element("DigitraxRoutesPreferences");  // NOI18N
149        digitraxRoutesPreferencesElement.setAttribute("noNamespaceSchemaLocation",  // NOI18N
150                "http://jmri.org/xml/schema/DigitraxRoutes.xsd",  // NOI18N
151                org.jdom2.Namespace.getNamespace("xsi",   // NOI18N
152                        "http://www.w3.org/2001/XMLSchema-instance"));  // NOI18N
153
154        Element routesEntity = new Element("routes");  // NOI18N
155        digitraxRoutesPreferencesElement.addContent(routesEntity);
156        routesEntity.setAttribute("class", "jmri.jmrix.loconet.configurexml.CmdStnRoutesManagerXml");  // NOI18N
157
158        routesEntity = new Element("routes");  // NOI18N
159        digitraxRoutesPreferencesElement.addContent(routesEntity);  // NOI18N
160        routesEntity.setAttribute("class", "Accy7thGenRoutes.configurexml.RoutesManagerXml");  // NOI18N
161
162        int countOfDevices =  l7garm.getCountOfDevicesWithRoutes();
163
164        log.debug("Saving {} devices",  l7garm.getCountOfDevicesWithRoutes());
165        if (countOfDevices < 1) {
166            log.debug("No Digitrax 7th-gen Accessory devices with stored/storable routes.");
167        } else {
168            StringBuilder loggingString = new StringBuilder("Known Digitrax 7th-gen Accessory devices with "
169                    + "stored/storable routes:\n");  // NOI18N
170            for (int aDevice = 0; aDevice < l7garm.getCountOfDevicesWithRoutes(); aDevice++) {
171                LnSimple7thGenDeviceRoutes device = l7garm.getDevice(aDevice);
172                loggingString.append("\tDevice ");  // NOI18N
173                loggingString.append(l7garm.getDevName(device.getDeviceType()));
174                loggingString.append(", ser. num. ");  // NOI18N
175                loggingString.append(device.getSerNum());
176                loggingString.append(", base addr. ");  // NOI18N
177                loggingString.append(device.getBaseAddr());
178                loggingString.append(".\n");
179                log.debug("doStore: saving {}\n",loggingString.toString());
180                Element deviceEntity = new Element("device");  // NOI18N
181                routesEntity.addContent(deviceEntity);
182
183                deviceEntity.setAttribute("type",   // NOI18N
184                        l7garm.getDevName(device.getDeviceType()));
185                deviceEntity.setAttribute("serNum",   // NOI18N
186                        Integer.toString(device.getSerNum()));
187                deviceEntity.setAttribute("baseAddr",    // NOI18N
188                        Integer.toString(device.getBaseAddr()));
189
190                // number of routes for the device
191                int numAvailRoutes =
192                        (device.getDeviceType() == LnConstants.RE_IPL_DIGITRAX_HOST_DS78V)
193                        ? 16 : 8;
194
195                for (int routeNumber = 0; routeNumber < numAvailRoutes; ++routeNumber) {
196                    log.debug("doStore: device {} route {} (of {} possible device routes)",
197                            aDevice, routeNumber+1, numAvailRoutes);
198                    // get route'loggingString "top" entry, to see if it is "used"
199                    if (device.
200                            getRoutes(routeNumber) != null) {
201                        LnSimpleRouteEntry re = device.getRoutes(routeNumber).getRouteEntry(0);
202                        if (re != null) {
203                            int turn = device.getRoutes(routeNumber).getRouteEntry(0).getNumber();
204                            log.debug("\tdoStore: route {} entry 0 turn = {}", routeNumber, turn);
205                            if ((turn >= 0) && (turn <= 2043)) {
206                                log.debug("\tdoStore: Continued at route {} with"
207                                        + " 1<=address<=2044: {}.",
208                                        routeNumber, turn);
209
210                                Element routeElement = new Element("route");  // NOI18N
211                                routeElement.setAttribute("number",   // NOI18N
212                                        Integer.toString((routeNumber+1)));
213                                loggingString.append("\t\tRoute ");  // NOI18N
214                                loggingString.append(routeNumber+1);
215                                loggingString.append(":\n");
216                                boolean anUndefined = false;
217                                for (int entryNumber = 0; (entryNumber < 8) &&
218                                        (!anUndefined); ++entryNumber) {
219                                    int addr = device.getRoutes(routeNumber).
220                                                    getRouteEntry(entryNumber).getNumber();
221                                    RouteSwitchPositionEnum state = device.getRoutes(routeNumber).
222                                                    getRouteEntry(entryNumber).getPosition();
223                                    if (state == null) {
224                                        state = RouteSwitchPositionEnum.UNUSED;
225                                        anUndefined = true;
226                                        log.debug("\tDevice {} Route {} loop entry {}"
227                                                + " has state {} ",
228                                                aDevice, routeNumber+1, entryNumber,
229                                                state.toString());
230                                    } else {
231                                        log.debug("\tDevice {} Route {} loop entry {} "
232                                                + "address {} has state {} ",
233                                                aDevice, routeNumber+1, entryNumber,
234                                                addr, state.toString());
235                                        Element routeEntryElement;
236                                        if (entryNumber == 0) {
237                                            routeEntryElement = new Element("routeTop");  // NOI18N
238                                            routeEntryElement.setAttribute("controlTurnout",  // NOI18N
239                                                    Integer.toString(addr+1));
240                                            routeEntryElement.setAttribute("controlTurnoutState",   // NOI18N
241                                                    state.toString() );
242                                            routeElement.addContent(routeEntryElement);
243                                            loggingString.append("\t\tTop Addr:");  // NOI18N
244                                            loggingString.append(Integer.toString(addr+1));
245                                        } else {
246                                            routeEntryElement = new Element("routeOutputTurnout");  // NOI18N
247                                            routeEntryElement.setAttribute("turnout",   // NOI18N
248                                                    Integer.toString(addr+1));
249                                            routeEntryElement.setAttribute("state",   // NOI18N
250                                                    state.toString());
251                                            routeElement.addContent(routeEntryElement);
252                                            loggingString.append("\tAddr:");    // NOI18N
253                                            loggingString.append(Integer.toString(addr+1));
254                                        }
255                                        loggingString.append(", State:");  // NOI18N
256                                        loggingString.append(state);
257                                    }
258                                    if ((entryNumber == 0) && (state != RouteSwitchPositionEnum.UNUSED)) {
259                                        deviceEntity.addContent(routeElement);
260
261                                    }
262                                }
263                                loggingString.append("\n");
264                            } else {
265                                log.debug("\tdoStore: no entries because top address is -1.");
266                            }
267                        }
268                    }
269                }
270            }
271            log.debug("doStore: saving string as: {}", loggingString.toString());
272        }
273
274        DigitraxRoutesXmlFile dtxXmlFil = new DigitraxRoutesXmlFile();
275        File fileFileFile = dtxXmlFil.getStoreFile();
276        Document xmlDoc = new Document(digitraxRoutesPreferencesElement);
277
278        log.debug("doStore: checking canwrite()");
279        if (!fileFileFile.canWrite()) {
280            log.debug("doStore: canwrite is false; Unable to create writable local Digitrax routes file");
281            try {
282                fileFileFile.createNewFile();
283            } catch (IOException e) {
284                log.error("IOError trying to create file!");
285                return false;
286            }
287            if (!fileFileFile.canWrite()) {
288                return false;
289            }
290        }
291
292        try {
293            dtxXmlFil.writeXML(fileFileFile, xmlDoc);
294        } catch (FileNotFoundException ex) {
295            log.error("File not found when writing", ex);  // NOI18N
296            return false;
297        } catch (IOException ex) {
298            log.error("IO Exception when writing", ex);  // NOI18N
299            return false;
300        }
301        return true;
302    }
303
304    public static class DigitraxRoutesXmlFile extends XmlFile {
305        private static final String PRODUCTIONFILEPATH = FileUtil.getProgramPath()
306                + "resources/digitraxRoutes/";  // NOI18N
307        private static final String USERFILEPATH = FileUtil.getUserFilesPath()
308                + "resources/digitraxRoutes/";  // NOI18N
309        private static final String TARGETFILEPATH = "DigitraxRoutes.xml";  // NOI18N
310
311        /**
312         * Getter.
313         * @return store file name including path
314         */
315        public static String getStoreFileName() {
316            return USERFILEPATH + TARGETFILEPATH;
317        }
318
319        /**
320         * Getter.
321         * @return load file name including path
322         */
323        public static String getLoadFileName() {
324            return getStoreFileName();
325        }
326
327        /**
328         * Getter
329         * @return store file
330         */
331        public File getStoreFile() {
332
333            log.debug("getStoreFile: check directory exists - {}", USERFILEPATH);
334            File chkdir = new File(USERFILEPATH);
335            log.debug("getStoreFile: check directory exists result: chkdir {}", chkdir);
336            if ((!chkdir.exists()) && (!chkdir.mkdirs())) {
337                log.warn("getStoreFile: check directory does not exist & make directory failed!");
338                return null;
339            }
340            File file = findFile(getStoreFileName());
341            log.debug("getStoreFile part n/2: file is {}.", file);
342
343            if (file == null) {
344                log.debug("getStoreFile: Try to Create new DigitraxRoutes.xml file");  // NOI18N
345                file = new File(getStoreFileName());
346                log.debug("getStoreFile part n: file is {}.", file);
347            } else {
348                try {
349                    FileUtil.rotate(file, 4, "bup");  // NOI18N
350                } catch (IOException ex) {
351                    log.debug("Rotate failed, reverting to xml backup");  // NOI18N
352                    makeBackupFile(getStoreFileName());
353                }
354            }
355            return file;
356        }
357
358        /**
359         * Getter.
360         * @return load file
361         */
362        public File getLoadFile() {
363            log.debug("getLoadFile: USERFILEPATH = {}, TARGETFILEPATH = {}",
364                    USERFILEPATH , TARGETFILEPATH);
365            File file = findFile(USERFILEPATH + TARGETFILEPATH);
366
367            if (file == null) {
368                file = findFile(PRODUCTIONFILEPATH + TARGETFILEPATH);
369                log.debug("getLoadFile: try2 - file = {}", file);
370            }
371            return file;
372        }
373    }
374
375    private final static Logger log = LoggerFactory.getLogger(Digitrax7thGenAccyRoutesXML.class);
376}