001package jmri.jmrix.openlcb.swing.eventtable.configurexml;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.util.List;
007
008import org.jdom2.Document;
009import org.jdom2.Element;
010import org.jdom2.JDOMException;
011
012import jmri.jmrit.XmlFile;
013//import jmri.jmrix.openlcb.OlcbEventNameStore;
014import jmri.jmrix.openlcb.swing.eventtable.EventTableDataModel;
015import jmri.util.FileUtil;
016
017import org.openlcb.EventID;
018import org.openlcb.NodeID;
019
020/**
021 * Persistence for the content of a EventTableDataModel
022 * <p>
023 * Note that this is explicitly invoked by 
024 * {@link jmri.jmrix.openlcb.swing.eventtable.EventTableDataModel}
025 * and is not invoked by the general "store all" mechanism used to store layout files.
026 *
027 * @author Bob Jacobsen Copyright (C) 2025, 2026
028 * @since 5.17.3
029 */
030public class EventTableDataModelXml extends XmlFile { // note final for testing
031
032    public EventTableDataModelXml(EventTableDataModel model) {
033        this.model = model;
034    }
035
036    EventTableDataModel model;
037
038    public void store() throws java.io.IOException {
039        log.debug("Storing using file: {}", getDefaultModelFileName());
040        createFile(getDefaultModelFileName(), true);
041        try {
042            writeFile(getDefaultModelFileName());
043        } catch (FileNotFoundException ex) {
044            log.error("File not found while writing Data Model file, may not be complete", ex);
045        }
046    }
047
048    public void load() {
049        log.debug("Loading...");
050        try {
051            readFile(getDefaultModelFileName());
052        } catch (JDOMException | IOException ex) {
053            log.error("Exception during Data Model file reading", ex);
054        }
055    }
056
057    private File createFile(String fileName, boolean backup) {
058        if (backup) {
059            makeBackupFile(fileName);
060        }
061
062        File file = null;
063        try {
064            if (!checkFile(fileName)) {
065                // The file does not exist, create it before writing
066                file = new File(fileName);
067                File parentDir = file.getParentFile();
068                if (!parentDir.exists()) {
069                    if (!parentDir.mkdir()) {
070                        log.error("Directory wasn't created");
071                    }
072                }
073                if (file.createNewFile()) {
074                    log.debug("New file created");
075                }
076           } else {
077              file = new File(fileName);
078           }
079       } catch (java.io.IOException ex) {
080          log.error("Exception while creating Event Name file, may not be complete", (Object) ex);
081       }
082       return file;
083   }
084
085   private void writeFile(String fileName) throws FileNotFoundException, java.io.IOException {
086        log.debug("writeFile {}", fileName);
087        // This is taken in large part from "Java and XML" page 368
088        File file = findFile(fileName);
089        if (file == null) {
090           file = new File(fileName);
091        }
092        // Create root element
093        Element root = new Element("eventTableStore");              // NOI18N
094        // root.setAttribute("noNamespaceSchemaLocation", // NOI18N
095        //      "http://jmri.org/xml/schema/idtags.xsd", // NOI18N
096        //      org.jdom2.Namespace.getNamespace("xsi", // NOI18N
097        //      "http://www.w3.org/2001/XMLSchema-instance")); // NOI18N
098        Document doc = newDocument(root);
099
100        // add XSLT processing instruction
101        // java.util.Map<String, String> m = new java.util.HashMap<>();
102        // m.put("type", "text/xsl"); // NOI18N
103        // m.put("href", xsltLocation + "idtags.xsl"); // NOI18N
104        // ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); // NOI18N
105        // doc.addContent(0, p);
106
107        Element values;
108
109        // Loop through node names
110        root.addContent(values = new Element("nodes")); // NOI18N
111        for (NodeID nid : model.getNodeIDMatches()) {
112            var name = model.getNodeName(nid);
113            log.debug("Writing node name: {} node {}", name, nid);
114            var element = new Element("node");
115            var nameElement = new Element("name");
116            nameElement.addContent(name);
117            var nodeIdElement = new Element("nodeID");
118            nodeIdElement.addContent(nid.toString());
119            element.addContent(nodeIdElement);
120            element.addContent(nameElement);
121            values.addContent(element);
122        }
123
124        // Loop through producer node <-> event ID associations
125        root.addContent(values = new Element("producers")); // NOI18N
126        for (var memo : EventTableDataModel.getMemos()) {
127            if (memo.eventID != null && memo.producer != null) {
128                var event = memo.eventID.toShortString();
129                var range = memo.rangeSuffix;
130                var producer = memo.producer.toString();
131                var pname = memo.producerName;
132                
133                var element = new Element("producer");
134                var eventElement = new Element("event");
135                eventElement.addContent(event);
136                var rangeElement = new Element("range");
137                rangeElement.addContent(range);
138                var producerElement = new Element("producer");
139                producerElement.addContent(producer);
140                var pnameElement = new Element("pName");
141                pnameElement.addContent(pname);
142                element.addContent(eventElement);
143                element.addContent(rangeElement);
144                element.addContent(producerElement);
145                element.addContent(pnameElement);
146                values.addContent(element);
147                
148            }
149        }
150
151        // Loop through consumer node <-> event ID associations
152        root.addContent(values = new Element("consumers")); // NOI18N
153        for (var memo : EventTableDataModel.getMemos()) {
154            if (memo.eventID != null && memo.consumer != null) {
155                var event = memo.eventID.toShortString();
156                var range = memo.rangeSuffix;
157                var consumer = memo.consumer.toString();
158                var cname = memo.consumerName;
159                
160                var element = new Element("consumer");
161                var eventElement = new Element("event");
162                eventElement.addContent(event);
163                var rangeElement = new Element("range");
164                rangeElement.addContent(range);
165                var consumerElement = new Element("consumer");
166                consumerElement.addContent(consumer);
167                var cnameElement = new Element("cName");
168                cnameElement.addContent(cname);
169                element.addContent(eventElement);
170                element.addContent(rangeElement);
171                element.addContent(consumerElement);
172                element.addContent(cnameElement);
173                values.addContent(element);
174                
175            }
176        }
177
178        // Store the event ID <-> Descriptions information
179        root.addContent(values = new Element("descriptions")); // NOI18N
180        for (var event : model.getAuxiliaryKnownEvents()) {
181            for (var description : model.getAuxiliaryInformation(event) ) {
182                log.trace("for event {} write {}", event, description);
183                var eventElement = new Element("event");
184                eventElement.addContent(event.toShortString());
185                var descriptionElement = new Element("description");
186                descriptionElement.addContent(description);
187                var valueElement = new Element("value");
188                values.addContent(valueElement);
189                valueElement.addContent(eventElement);
190                valueElement.addContent(descriptionElement);
191            }
192        }
193
194        writeXML(file, doc);
195    }
196
197    protected String getDefaultModelFileName() { // protected for testing
198        return getFileLocation() + getModelFileDirectoryName() + File.separator + getModelDataFileName();
199    }
200    
201    private String getFileLocation() {
202        return FileUtil.getUserFilesPath();
203    }
204
205    private static final String MODEL_FILE_DIRECTORY_NAME = "eventnames"; // NOI18N
206
207    protected String getModelFileDirectoryName() { // protected for testing
208        return MODEL_FILE_DIRECTORY_NAME;
209    }
210
211    private String getModelDataFileName() {
212        return "eventTable.xml";
213    }
214    
215    private void readFile(String fileName) throws org.jdom2.JDOMException, java.io.IOException, IllegalArgumentException {
216        // Check file exists
217        if (findFile(fileName) == null) {
218            log.debug("{} file could not be found", fileName);
219            return;
220        }
221
222        // Find root
223        Element root = rootFromName(fileName);
224        if (root == null) {
225            log.warn("{} file could not be read", fileName);
226            return;
227        }
228
229        // Now read name-id mapping information
230        if (root.getChild("nodes") != null) { // NOI18N
231            List<Element> nodes = root.getChildren("nodes");
232            log.debug("readFile sees {} nodes elements", nodes.size());
233            for (Element n : nodes) {
234                List<Element> l = n.getChildren("node"); // NOI18N
235                log.debug("    readFile sees {} node names", l.size());
236                for (Element e : l) {
237                    String nid = e.getChild("nodeID").getText(); // NOI18N
238                    String name = e.getChild("name").getText();
239                    log.trace("        read nodeID {} name {}", nid, name);
240                    model.addMatch(new NodeID(nid), name);
241                }
242            }
243        }
244
245        // Now read producer information
246        if (root.getChild("producers") != null) { // NOI18N
247            List<Element> producers = root.getChildren("producers");
248            log.debug("readFile sees {} producers elements", producers.size());
249            for (Element n : producers) {
250                List<Element> l = n.getChildren("producer"); // NOI18N
251                for (Element e : l) {
252                    String event = e.getChild("event").getText(); // NOI18N
253                    String range = e.getChild("range").getText(); // NOI18N
254                    String producer = e.getChild("producer").getText(); // NOI18N
255                    String pName = e.getChild("pName").getText(); // NOI18N
256                    
257                    // having those, we see if there's a node ID associated with the name
258                    NodeID newNodeID = null;
259                    if (! pName.isEmpty()) {
260                        newNodeID = model.getNodeID(pName);
261                    }
262                    if (newNodeID == null) {
263                        // no, use the ID from the file
264                        newNodeID = new NodeID(producer);
265                    }
266
267                    model.recordProducer(new EventID(event), newNodeID, range, false);
268                }
269            }
270        }
271
272        // Now read consumer information
273        if (root.getChild("consumers") != null) { // NOI18N
274            List<Element> consumers = root.getChildren("consumers");
275            log.debug("readFile sees {} consumers elements", consumers.size());
276            for (Element n : consumers) {
277                List<Element> l = n.getChildren("consumer"); // NOI18N
278                for (Element e : l) {
279                    String event = e.getChild("event").getText(); // NOI18N
280                    String range = e.getChild("range").getText(); // NOI18N
281                    String consumer = e.getChild("consumer").getText(); // NOI18N
282                    String cName = e.getChild("cName").getText(); // NOI18N
283
284                    // having those, we see if there's a node ID associated with the name
285                    NodeID newNodeID = null;
286                    if (! cName.isEmpty()) {
287                        newNodeID = model.getNodeID(cName);
288                    }
289                    if (newNodeID == null) {
290                        // no, use the ID from the file
291                        newNodeID = new NodeID(consumer);
292                    }
293
294                    model.recordConsumer(new EventID(event), newNodeID, range);
295                }
296            }
297        }
298
299        // Now read descriptions information
300        if (root.getChild("descriptions") != null) { // NOI18N
301            List<Element> descriptions = root.getChildren("descriptions");
302            log.debug("readFile sees {} description elements", descriptions.size());
303            for (Element n : descriptions) {
304                List<Element> l = n.getChildren("value"); // NOI18N
305                for (Element e : l) {
306                    String event = e.getChild("event").getText(); // NOI18N
307                    String description = e.getChild("description").getText(); // NOI18N
308
309                    model.addAuxiliaryInformation(new EventID(event), description);
310                }
311            }
312        }
313
314    }
315
316    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EventTableDataModelXml.class);
317
318}