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