001package jmri.jmrix.openlcb.configurexml;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.util.List;
007import java.util.HashSet;
008
009import org.jdom2.Document;
010import org.jdom2.Element;
011import org.jdom2.JDOMException;
012
013import jmri.IdTagManager;
014import jmri.InstanceManager;
015import jmri.jmrit.XmlFile;
016import jmri.jmrix.openlcb.OlcbConstants;
017import jmri.jmrix.openlcb.OlcbEventNameStore;
018import jmri.util.FileUtil;
019
020import org.openlcb.EventID;
021
022/**
023 * JMRI's implementation of part of the OpenLcb EventNameStore interface persistance
024 *
025 * @author Bob Jacobsen Copyright (C) 2025
026 */
027public final class OlcbEventNameStoreXml extends XmlFile {
028
029    public OlcbEventNameStoreXml(OlcbEventNameStore nameStore) {
030        this.nameStore = nameStore;
031    }
032
033    OlcbEventNameStore nameStore;
034    /**
035     * The original implementation of this store
036     * was via the IdTagManager class.  This is now
037     * viewed as a mistake.  This method takes names
038     * stored in the IdTagManager and migrates them
039     * to the dedicated local store.
040     *<p>
041     * Note when this actually migrates, it opens and loads the IdTag Manager
042     * which can result in name confusion when Reporters
043     * are later created by a panel file.  If this has already happened,
044     * the internal reporters generated by that have to be removed
045     * before this migration takes place. This means that there's
046     * no real good place to run this as part of  JMRI startup.
047     * Instead, it's left as a public method so it can (when needed)
048     * be invoked by e.g. a script after the name clean up has been done.
049     */
050    public void migrateFromIdTagStore() {
051        // check for whether migration is already done
052        File file = findFile(getDefaultEventNameFileName());
053        if (file != null) {
054           return;
055        }
056
057        log.info("Checking for IdTag-stored Event Names to migrate");
058        
059        // get the IdTags, find any that are Event Names, and migrate those
060        IdTagManager tagmgr = InstanceManager.getDefault(IdTagManager.class); 
061        log.debug("*** Starting event name migration");
062        
063        var tagSet = tagmgr.getNamedBeanSet();
064        
065        var localSet = new HashSet<>(tagSet); // avoid concurrent modifications
066        
067        for (var tag : localSet) {
068            log.trace("  Process tag {}", tag);
069            if (tag.getSystemName().startsWith(OlcbConstants.tagPrefix)) {
070                var eid = tag.getSystemName().substring(OlcbConstants.tagPrefix.length());
071                log.info("    Migrating event name '{}' event ID '{}' from IdTag table", 
072                        tag.getUserName(), eid);
073                
074                // Add to this store
075                nameStore.addMatch(new EventID(eid), tag.getUserName());
076                
077                // Remove from ID tag store
078                tagmgr.deregister(tag);
079                tag.dispose();
080            }
081        }
082        
083        log.debug("*** Ending event name migration");
084    }
085
086    public void store() throws java.io.IOException {
087        log.debug("Storing using file: {}", getDefaultEventNameFileName());
088        createFile(getDefaultEventNameFileName(), true);
089        try {
090            writeFile(getDefaultEventNameFileName());
091        } catch (FileNotFoundException ex) {
092            log.error("File not found while writing Event Name file, may not be complete", ex);
093        }
094    }
095
096    public void load() {
097        log.debug("Loading...");
098        var wasDirty = nameStore.dirty;
099        try {
100            readFile(getDefaultEventNameFileName());
101        } catch (JDOMException | IOException ex) {
102            log.error("Exception during IdTag file reading", ex);
103        }
104        nameStore.dirty = wasDirty;
105    }
106
107    private File createFile(String fileName, boolean backup) {
108        if (backup) {
109            makeBackupFile(fileName);
110        }
111
112        File file = null;
113        try {
114            if (!checkFile(fileName)) {
115                // The file does not exist, create it before writing
116                file = new File(fileName);
117                File parentDir = file.getParentFile();
118                if (!parentDir.exists()) {
119                    if (!parentDir.mkdir()) {
120                        log.error("Directory wasn't created");
121                    }
122                }
123                if (file.createNewFile()) {
124                    log.debug("New file created");
125                }
126           } else {
127              file = new File(fileName);
128           }
129       } catch (java.io.IOException ex) {
130          log.error("Exception while creating Event Name file, may not be complete", (Object) ex);
131       }
132       return file;
133   }
134
135   private void writeFile(String fileName) throws FileNotFoundException, java.io.IOException {
136        log.debug("writeFile {}", fileName);
137        // This is taken in large part from "Java and XML" page 368
138        File file = findFile(fileName);
139        if (file == null) {
140           file = new File(fileName);
141        }
142        // Create root element
143        Element root = new Element("eventNameStore");              // NOI18N
144        // root.setAttribute("noNamespaceSchemaLocation", // NOI18N
145        //      "http://jmri.org/xml/schema/idtags.xsd", // NOI18N
146        //      org.jdom2.Namespace.getNamespace("xsi", // NOI18N
147        //      "http://www.w3.org/2001/XMLSchema-instance")); // NOI18N
148        Document doc = newDocument(root);
149
150        // add XSLT processing instruction
151        // java.util.Map<String, String> m = new java.util.HashMap<>();
152        // m.put("type", "text/xsl"); // NOI18N
153        // m.put("href", xsltLocation + "idtags.xsl"); // NOI18N
154        // ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); // NOI18N
155        // doc.addContent(0, p);
156
157        Element values;
158
159        // Loop through event names
160        root.addContent(values = new Element("names")); // NOI18N
161        for (EventID eid : nameStore.getMatches()) {
162            var name = nameStore.getEventName(eid);
163            log.debug("Writing event name: {} event {}", name, eid);
164            var element = new Element("entry");
165            var nameElement = new Element("name");
166            nameElement.addContent(name);
167            var eventIdElement = new Element("eventID");
168            eventIdElement.addContent(eid.toShortString());
169            element.addContent(eventIdElement);
170            element.addContent(nameElement);
171            values.addContent(element);
172        }
173        writeXML(file, doc);
174    }
175
176    private String getDefaultEventNameFileName() {
177        return getFileLocation() + getEventNameDirectoryName() + File.separator + getEventNameFileName();
178    }
179    
180    private String getFileLocation() {
181        return FileUtil.getUserFilesPath();
182    }
183
184    private static final String EVENT_NAMES_DIRECTORY_NAME = "eventnames"; // NOI18N
185
186    private String getEventNameDirectoryName() {
187        return EVENT_NAMES_DIRECTORY_NAME;
188    }
189
190    private String getEventNameFileName() {
191        return "eventNames.xml";
192    }
193    
194    private void readFile(String fileName) throws org.jdom2.JDOMException, java.io.IOException, IllegalArgumentException {
195        // Check file exists
196        if (findFile(fileName) == null) {
197            log.debug("{} file could not be found", fileName);
198            return;
199        }
200
201        // Find root
202        Element root = rootFromName(fileName);
203        if (root == null) {
204            log.debug("{} file could not be read", fileName);
205            return;
206        }
207
208        // Now read name-id mapping information
209        if (root.getChild("names") != null) { // NOI18N
210            List<Element> names = root.getChildren("names");
211            log.debug("readFile sees {} names elements", names.size());
212            for (Element n : names) {
213                List<Element> l = n.getChildren("entry"); // NOI18N
214                log.debug("    readFile sees {} event names", l.size());
215                for (Element e : l) {
216                    String eid = e.getChild("eventID").getText(); // NOI18N
217                    String name = e.getChild("name").getText();
218                    log.debug("        read EventID {}", eid);
219                    nameStore.addMatch(new EventID(eid), name);
220                }
221            }
222        }
223    }
224
225
226    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbEventNameStoreXml.class);
227
228}