/** * Set the default location for the Roster file, and all individual locomotive files. * * @param f Absolute pathname to use. A null or "" argument flags a return to the original default * in the user's files directory. This parameter must be a potentially valid path on the * system. */ public void setRosterLocation(String f) { String oldRosterLocation = this.rosterLocation; String p = f; if (p != null) { if (p.isEmpty()) { p = null; } else { p = FileUtil.getAbsoluteFilename(p); if (p == null) { throw new IllegalArgumentException( Bundle.getMessage("IllegalRosterLocation", f)); // NOI18N } if (!p.endsWith(File.separator)) { p = p + File.separator; } } } if (p == null) { p = FileUtil.getUserFilesPath(); } this.rosterLocation = p; log.debug("Setting roster location from {} to {}", oldRosterLocation, this.rosterLocation); if (this.rosterLocation.equals(FileUtil.getUserFilesPath())) { log.debug("Roster location reset to default"); } if (!this.rosterLocation.equals(oldRosterLocation)) { this.firePropertyChange( RosterConfigManager.DIRECTORY, oldRosterLocation, this.rosterLocation); } this.reloadRosterFile(); }
/** * Configure the {@link jmri.profile.Profile} to use for this application. * * <p>Overrides super() method so dialogs can be displayed. */ @Override protected void configureProfile() { String profileFilename; FileUtil.createDirectory(FileUtil.getPreferencesPath()); // Needs to be declared final as we might need to // refer to this on the Swing thread File profileFile; profileFilename = getConfigFileName().replaceFirst(".xml", ".properties"); // decide whether name is absolute or relative if (!new File(profileFilename).isAbsolute()) { // must be relative, but we want it to // be relative to the preferences directory profileFile = new File(FileUtil.getPreferencesPath() + profileFilename); } else { profileFile = new File(profileFilename); } ProfileManager.getDefault().setConfigFile(profileFile); // See if the profile to use has been specified on the command line as // a system property jmri.profile as a profile id. if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) { ProfileManager.getDefault() .setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY)); } // @see jmri.profile.ProfileManager#migrateToProfiles JavaDoc for conditions handled here if (!ProfileManager.getDefault().getConfigFile().exists()) { // no profile config for this app try { if (ProfileManager.getDefault() .migrateToProfiles(getConfigFileName())) { // migration or first use // notify user of change only if migration occured // TODO: a real migration message JOptionPane.showMessageDialog( sp, Bundle.getMessage("ConfigMigratedToProfile"), jmri.Application.getApplicationName(), JOptionPane.INFORMATION_MESSAGE); } } catch (IOException | IllegalArgumentException ex) { JOptionPane.showMessageDialog( sp, ex.getLocalizedMessage(), jmri.Application.getApplicationName(), JOptionPane.ERROR_MESSAGE); log.error(ex.getMessage(), ex); } } try { ProfileManagerDialog.getStartingProfile(sp); // Manually setting the configFilename property since calling // Apps.setConfigFilename() does not reset the system property System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME); log.info("Starting with profile {}", ProfileManager.getDefault().getActiveProfile().getId()); } catch (IOException ex) { log.info( "Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage()); } }
/** Get an array of all the RosterEntry-containing files in the target directory */ static String[] getAllFileNames() { // ensure preferences will be found for read FileUtil.createDirectory(LocoFile.getFileLocation()); // create an array of file names from roster dir in preferences, count entries int i; int np = 0; String[] sp = null; if (log.isDebugEnabled()) { log.debug("search directory " + LocoFile.getFileLocation()); } File fp = new File(LocoFile.getFileLocation()); if (fp.exists()) { sp = fp.list(); if (sp != null) { for (i = 0; i < sp.length; i++) { if (sp[i].endsWith(".xml") || sp[i].endsWith(".XML")) { np++; } } } else { log.warn("expected directory, but {} was a file", LocoFile.getFileLocation()); } } else { log.warn( FileUtil.getUserFilesPath() + "roster directory was missing, though tried to create it"); } // Copy the entries to the final array String sbox[] = new String[np]; int n = 0; if (sp != null && np > 0) { for (i = 0; i < sp.length; i++) { if (sp[i].endsWith(".xml") || sp[i].endsWith(".XML")) { sbox[n++] = sp[i]; } } } // The resulting array is now sorted on file-name to make it easier // for humans to read jmri.util.StringUtil.sort(sbox); if (log.isDebugEnabled()) { log.debug("filename list:"); for (i = 0; i < sbox.length; i++) { log.debug(" " + sbox[i]); } } return sbox; }
@Override protected void setAndLoadPreferenceFile() { File sharedConfig = null; try { sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG); if (!sharedConfig.canRead()) { sharedConfig = null; } } catch (FileNotFoundException ex) { // ignore - this only means that sharedConfig does not exist. } super.setAndLoadPreferenceFile(); if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) { // this was logged in the super method if (!GraphicsEnvironment.isHeadless()) { JOptionPane.showMessageDialog( sp, Bundle.getMessage( "SingleConfigMigratedToSharedConfig", ProfileManager.getDefault().getActiveProfile().getName()), jmri.Application.getApplicationName(), JOptionPane.INFORMATION_MESSAGE); } } }
protected void debugMenu(JMenuBar menuBar, WindowInterface wi) { JMenu d = new DebugMenu(this); // also add some tentative items from jmrix d.add(new JSeparator()); d.add(new jmri.jmrix.pricom.PricomMenu()); d.add(new JSeparator()); d.add(new jmri.jmrix.jinput.treecontrol.TreeAction()); d.add(new jmri.jmrix.libusb.UsbViewAction()); d.add(new JSeparator()); try { d.add( new RunJythonScript( "RailDriver Throttle", new File(FileUtil.findURL("jython/RailDriver.py").toURI()))); } catch (URISyntaxException | NullPointerException ex) { log.error("Unable to load RailDriver Throttle", ex); JMenuItem i = new JMenuItem("RailDriver Throttle"); i.setEnabled(false); d.add(i); } // also add some tentative items from webserver d.add(new JSeparator()); d.add(new WebServerAction()); d.add(new JSeparator()); d.add(new WiThrottleCreationAction()); menuBar.add(d); }
public void printSwitchList(Location location, boolean isPreview) { File buildFile = TrainManagerXml.instance().getSwitchListFile(location.getName()); if (!buildFile.exists()) { log.warn("Switch list file missing for location ({})", location.getName()); return; } if (isPreview && Setup.isManifestEditorEnabled()) { TrainPrintUtilities.openDesktopEditor(buildFile); } else { TrainPrintUtilities.printReport( buildFile, location.getName(), isPreview, Setup.getFontName(), false, FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(), Setup.getSwitchListOrientation(), Setup.getManifestFontSize()); } if (!isPreview) { location.setStatus(Location.PRINTED); location.setSwitchListState(Location.SW_PRINTED); } }
public void actionPerformed(ActionEvent e) { // obtain a HardcopyWriter to do this Roster r = Roster.instance(); String title = "DecoderPro Roster"; String rosterGroup = r.getDefaultRosterGroup(); // rosterGroup may legitimately be null // but getProperty returns null if the property cannot be found, so // we test that the property exists before attempting to get its value if (Beans.hasProperty(wi, RosterGroupSelector.SELECTED_ROSTER_GROUP)) { rosterGroup = (String) Beans.getProperty(wi, RosterGroupSelector.SELECTED_ROSTER_GROUP); } if (rosterGroup == null) { title = title + " All Entries"; } else { title = title + " Group " + rosterGroup + " Entires"; } HardcopyWriter writer = null; try { writer = new HardcopyWriter(mFrame, title, 10, .5, .5, .5, .5, isPreview); } catch (HardcopyWriter.PrintCanceledException ex) { log.debug("Print cancelled"); return; } // add the image ImageIcon icon = new ImageIcon(FileUtil.findURL("resources/decoderpro.gif", FileUtil.Location.INSTALLED)); // we use an ImageIcon because it's guaranteed to have been loaded when ctor is complete writer.write(icon.getImage(), new JLabel(icon)); // Add a number of blank lines, so that the roster entry starts below the decoderpro logo int height = icon.getImage().getHeight(null); int blanks = (height - writer.getLineAscent()) / writer.getLineHeight(); try { for (int i = 0; i < blanks; i++) { String s = "\n"; writer.write(s, 0, s.length()); } } catch (IOException ex) { log.warn("error during printing: " + ex); } // Loop through the Roster, printing as needed List<RosterEntry> l = r.matchingList(null, null, null, null, null, null, null); // take all log.debug("Roster list size: " + l.size()); for (RosterEntry re : l) { if (rosterGroup != null) { if (re.getAttribute(Roster.getRosterGroupProperty(rosterGroup)) != null && re.getAttribute(Roster.getRosterGroupProperty(rosterGroup)).equals("yes")) { re.printEntry(writer); } } else { re.printEntry(writer); } } // and force completion of the printing writer.close(); }
/** * Find a file by looking * * <UL> * <LI>in xml/layout/ in the preferences directory, if that exists * <LI>in xml/layout/ in the application directory, if that exists * <LI>in xml/ in the preferences directory, if that exists * <LI>in xml/ in the application directory, if that exists * <LI>at top level in the application directory * <LI> * </ul> * * @param f Local filename, perhaps without path information * @return Corresponding File object */ @Override public URL find(String f) { URL u = FileUtil.findURL(f, "xml/layout", "xml"); // NOI18N if (u == null) { this.locateFileFailed(f); } return u; }
/** * Fill in the logo and status panel * * @return Properly-filled out JPanel */ protected JPanel statusPanel() { JPanel pane1 = new JPanel(); pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS)); log.debug("Fetch main logo: {}", logo()); pane1.add( new JLabel( new ImageIcon( getToolkit().getImage(FileUtil.findURL(logo(), FileUtil.Location.INSTALLED)), "JMRI logo"), JLabel.LEFT)); pane1.add( Box.createRigidArea(new Dimension(15, 0))); // Some spacing between logo and status panel log.debug("start labels"); JPanel pane2 = new JPanel(); pane2.setLayout(new BoxLayout(pane2, BoxLayout.Y_AXIS)); pane2.add(new JLabel(line1())); pane2.add(new JLabel(line2())); pane2.add(new JLabel(line3())); pane2.add( new JLabel( Bundle.getMessage( "ActiveProfile", ProfileManager.getDefault().getActiveProfile().getName()))); // add listerner for Com port updates ConnectionStatus.instance().addPropertyChangeListener(this); int i = 0; for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) { if (!conn.getDisabled()) { connection[i] = conn; i++; } if (i > 3) { break; } } buildLine4(pane2); buildLine5(pane2); buildLine6(pane2); buildLine7(pane2); pane2.add(new JLabel(line8())); pane2.add(new JLabel(line9())); pane1.add(pane2); return pane1; }
// main entry point to run standalone public static void main(String[] args) { String logFile = "default.lcf"; try { if (new java.io.File(logFile).canRead()) { org.apache.log4j.PropertyConfigurator.configure("default.lcf"); } else { org.apache.log4j.BasicConfigurator.configure(); } } catch (java.lang.NoSuchMethodError e) { System.out.println("Exception starting logging: " + e); } // print the location where the result is stored System.out.println(jmri.util.FileUtil.getUserFilesPath() + "decoderIndex.xml"); // recreate the index DecoderIndexCreateAction da = new DecoderIndexCreateAction(null); da.setIncrement(true); da.actionPerformed(null); }
@Override public void actionPerformed(ActionEvent event) { Roster roster = Roster.instance(); String rosterGroup = Roster.instance().getDefaultRosterGroup(); RosterEntry[] entries; // rosterGroup may legitimately be null // but getProperty returns null if the property cannot be found, so // we test that the property exists before attempting to get its value if (Beans.hasProperty(wi, RosterGroupSelector.SELECTED_ROSTER_GROUP)) { rosterGroup = (String) Beans.getProperty(wi, RosterGroupSelector.SELECTED_ROSTER_GROUP); log.debug("selectedRosterGroup was {}", rosterGroup); } if (Beans.hasProperty(wi, "selectedRosterEntries")) { entries = (RosterEntry[]) Beans.getProperty(wi, "selectedRosterEntries"); if (entries != null) { log.debug("selectedRosterEntries found {} entries", entries.length); } else { log.debug("selectedRosterEntries left entries null"); } } else { entries = selectRosterEntry(rosterGroup); if (entries != null) { log.debug("selectRosterEntry(rosterGroup) found {} entries", entries.length); } else { log.debug("selectRosterEntry(rosterGroup) left entries null"); } } if (entries == null) { return; } // get parent object if there is one // Component parent = null; // if ( event.getSource() instanceof Component) parent = (Component)event.getSource(); // find the file for the selected entry for (RosterEntry re : entries) { String filename = roster.fileFromTitle(re.titleString()); String fullFilename = LocoFile.getFileLocation() + filename; log.debug("resolves to [{}], [{}]", filename, fullFilename); // prompt for one last chance log.debug("rosterGroup now {}", rosterGroup); if (rosterGroup == null) { if (!userOK(re.titleString(), filename, fullFilename)) { return; } // delete it from roster roster.removeEntry(re); } else { String group = Roster.getRosterGroupProperty(rosterGroup); log.debug("removing {} group from entry", group); re.deleteAttribute(group); re.updateFile(); } Roster.writeRosterFile(); // backup the file & delete it if (rosterGroup == null) { try { // ensure preferences will be found FileUtil.createDirectory(LocoFile.getFileLocation()); // move original file to backup LocoFile df = new LocoFile(); // need a dummy object to do this operation in next line df.makeBackupFile(LocoFile.getFileLocation() + filename); } catch (Exception ex) { log.error("error during locomotive file output: " + ex); } } } }
@Override public boolean loadDeferred(File fi) { return this.loadDeferred(FileUtil.fileToURL(fi)); }
/** * Load a file. * * <p>Handles problems locally to the extent that it can, by routing them to the * creationErrorEncountered method. * * @param fi file to load * @param registerDeferred true to register objects to defer * @return true if no problems during the load * @throws JmriConfigureXmlException * @see jmri.configurexml.XmlAdapter#loadDeferred() * @since 2.11.2 */ @Override public boolean load(File fi, boolean registerDeferred) throws JmriConfigureXmlException { return this.load(FileUtil.fileToURL(fi), registerDeferred); }
/** * Provides the mechanisms for storing an entire layout configuration to XML. "Layout" refers to the * hardware: Specific communcation systems, etc. * * @see <A HREF="package-summary.html">Package summary for details of the overall structure</A> * @author Bob Jacobsen Copyright (c) 2002, 2008 * @version $Revision$ */ public class ConfigXmlManager extends jmri.jmrit.XmlFile implements jmri.ConfigureManager { /** * Define the current schema version string for the layout-config schema. See the <A * HREF="package-summary.html#schema">Schema versioning discussion</a>. Also controls the * stylesheet file version. */ public static final String schemaVersion = "-2-9-6"; public ConfigXmlManager() {} public void registerConfig(Object o) { registerConfig(o, 50); } public void registerPref(Object o) { // skip if already present, leaving in original order if (plist.contains(o)) { return; } confirmAdapterAvailable(o); // and add to list plist.add(o); } /** * Common check routine to confirm an adapter is available as part of registration process. Only * enabled when Log4J DEBUG level is selected, to load fewer classes at startup. */ void confirmAdapterAvailable(Object o) { if (log.isDebugEnabled()) { String adapter = adapterName(o); if (log.isDebugEnabled()) { log.debug("register " + o + " adapter " + adapter); } if (adapter != null) { try { Class.forName(adapter); } catch (java.lang.ClassNotFoundException ex) { locateClassFailed(ex, adapter, o); } catch (java.lang.NoClassDefFoundError ex) { locateClassFailed(ex, adapter, o); } } } } /** * Remove the registered preference items. This is used e.g. when a GUI wants to replace the * preferences with new values. */ public void removePrefItems() { if (log.isDebugEnabled()) { log.debug("removePrefItems dropped " + plist.size()); } plist.clear(); } public Object findInstance(Class<?> c, int index) { ArrayList<Object> temp = new ArrayList<Object>(plist); temp.addAll(clist.keySet()); temp.addAll(tlist); temp.addAll(ulist); temp.addAll(uplist); for (int i = 0; i < temp.size(); i++) { if (c.isInstance(temp.get(i))) { if (index-- == 0) { return temp.get(i); } } } return null; } public ArrayList<Object> getInstanceList(Class<?> c) { ArrayList<Object> temp = new ArrayList<Object>(plist); ArrayList<Object> returnlist = new ArrayList<Object>(); temp.addAll(clist.keySet()); temp.addAll(tlist); temp.addAll(ulist); temp.addAll(uplist); for (int i = 0; i < temp.size(); i++) { if (c.isInstance(temp.get(i))) { returnlist.add(temp.get(i)); // if (index-- == 0) return temp.get(i); } } if (returnlist.isEmpty()) { return null; } return returnlist; } public void registerConfig(Object o, int x) { // skip if already present, leaving in original order if (clist.containsKey(o)) { return; } confirmAdapterAvailable(o); // and add to list // clist.add(o); clist.put(o, x); } public void registerTool(Object o) { // skip if already present, leaving in original order if (tlist.contains(o)) { return; } confirmAdapterAvailable(o); // and add to list tlist.add(o); } /** * Register an object whose state is to be tracked. It is not an error if the original object was * already registered. * * @param o The object, which must have an associated adapter class. */ public void registerUser(Object o) { // skip if already present, leaving in original order if (ulist.contains(o)) { return; } confirmAdapterAvailable(o); // and add to list ulist.add(o); } public void registerUserPrefs(Object o) { // skip if already present, leaving in original order if (uplist.contains(o)) { return; } confirmAdapterAvailable(o); // and add to list uplist.add(o); } public void deregister(Object o) { plist.remove(o); if (o != null) { clist.remove(o); } tlist.remove(o); ulist.remove(o); uplist.remove(o); } ArrayList<Object> plist = new ArrayList<Object>(); // Hashtable<Object, Integer> clist = new Hashtable<Object, Integer>(); Map<Object, Integer> clist = Collections.synchronizedMap(new LinkedHashMap<Object, Integer>()); ArrayList<Object> tlist = new ArrayList<Object>(); ArrayList<Object> ulist = new ArrayList<Object>(); ArrayList<Object> uplist = new ArrayList<Object>(); private ArrayList<Element> loadDeferredList = new ArrayList<Element>(); /** * Find the name of the adapter class for an object. * * @param o object of a configurable type * @return class name of adapter */ public static String adapterName(Object o) { String className = o.getClass().getName(); if (log.isDebugEnabled()) { log.debug("handle object of class " + className); } int lastDot = className.lastIndexOf("."); String result = null; if (lastDot > 0) { // found package-class boundary OK result = className.substring(0, lastDot) + ".configurexml." + className.substring(lastDot + 1, className.length()) + "Xml"; if (log.isDebugEnabled()) { log.debug("adapter class name is " + result); } return result; } else { // no last dot found! log.error("No package name found, which is not yet handled!"); return null; } } /** * Handle failure to load adapter class. Although only a one-liner in this class, it is a separate * member to facilitate testing. */ void locateClassFailed(Throwable ex, String adapterName, Object o) { log.error(ex.getMessage() + " could not load adapter class " + adapterName); if (log.isDebugEnabled()) { ex.printStackTrace(); } } protected Element initStore() { Element root = new Element("layout-config"); root.setAttribute( "noNamespaceSchemaLocation", "http://jmri.org/xml/schema/layout" + schemaVersion + ".xsd", org.jdom2.Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")); return root; } protected void addPrefsStore(Element root) { for (int i = 0; i < plist.size(); i++) { Object o = plist.get(i); Element e = elementFromObject(o); if (e != null) { root.addContent(e); } } } protected boolean addConfigStore(Element root) { boolean result = true; ArrayList<Map.Entry<Object, Integer>> l = new ArrayList<Map.Entry<Object, Integer>>(clist.entrySet()); Collections.sort( l, new Comparator<Map.Entry<Object, Integer>>() { public int compare(Map.Entry<Object, Integer> o1, Map.Entry<Object, Integer> o2) { return o1.getValue().compareTo(o2.getValue()); } }); for (int i = 0; i < l.size(); i++) { try { Object o = l.get(i).getKey(); Element e = elementFromObject(o); if (e != null) { root.addContent(e); } } catch (java.lang.Exception e) { storingErrorEncountered( null, "storing to file", "Unknown error (Exception)", null, null, e); result = false; } } return result; } protected boolean addToolsStore(Element root) { boolean result = true; for (int i = 0; i < tlist.size(); i++) { Object o = tlist.get(i); try { Element e = elementFromObject(o); if (e != null) { root.addContent(e); } } catch (java.lang.Exception e) { result = false; storingErrorEncountered( ((XmlAdapter) o), "storing to file", "Unknown error (Exception)", null, null, e); } } return result; } protected boolean addUserStore(Element root) { boolean result = true; for (int i = 0; i < ulist.size(); i++) { Object o = ulist.get(i); try { Element e = elementFromObject(o); if (e != null) { root.addContent(e); } } catch (java.lang.Exception e) { result = false; storingErrorEncountered( (XmlAdapter) o, "storing to file", "Unknown error (Exception)", null, null, e); } } return result; } protected void addUserPrefsStore(Element root) { for (int i = 0; i < uplist.size(); i++) { Object o = uplist.get(i); Element e = elementFromObject(o); if (e != null) { root.addContent(e); } } } protected void includeHistory(Element root) { // add history to end of document if (InstanceManager.getDefault(FileHistory.class) != null) { root.addContent( jmri.jmrit.revhistory.configurexml.FileHistoryXml.storeDirectly( InstanceManager.getDefault(FileHistory.class))); } } protected boolean finalStore(Element root, File file) { try { // Document doc = newDocument(root, dtdLocation+"layout-config-"+dtdVersion+".dtd"); Document doc = newDocument(root); // add XSLT processing instruction // <?xml-stylesheet type="text/xsl" href="XSLT/panelfile"+schemaVersion+".xsl"?> java.util.Map<String, String> m = new java.util.HashMap<String, String>(); m.put("type", "text/xsl"); m.put("href", xsltLocation + "panelfile" + schemaVersion + ".xsl"); ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); doc.addContent(0, p); // add version at front storeVersion(root); writeXML(file, doc); } catch (java.io.FileNotFoundException ex3) { storingErrorEncountered( null, "storing to file " + file.getName(), "File not found " + file.getName(), null, null, ex3); log.error("FileNotFound error writing file: " + ex3.getLocalizedMessage()); return false; } catch (java.io.IOException ex2) { storingErrorEncountered( null, "storing to file " + file.getName(), "IO error writing file " + file.getName(), null, null, ex2); log.error("IO error writing file: " + ex2.getLocalizedMessage()); return false; } return true; } /** * Writes config, tools and user to a file. * * @param file */ public boolean storeAll(File file) { boolean result = true; Element root = initStore(); if (!addConfigStore(root)) { result = false; } if (!addToolsStore(root)) { result = false; } if (!addUserStore(root)) { result = false; } addConfigStore(root); addToolsStore(root); addUserStore(root); includeHistory(root); if (!finalStore(root, file)) { result = false; } return result; } /** Writes prefs to a predefined File location. */ public void storePrefs() { storePrefs(prefsFile); } public void storePrefs(File file) { synchronized (this) { Element root = initStore(); addPrefsStore(root); finalStore(root, file); } } public void storeUserPrefs(File file) { synchronized (this) { Element root = initStore(); addUserPrefsStore(root); finalStore(root, file); } } /** * Set location for preferences file. * * <p>File need not exist, but location must be writable when storePrefs() called. */ public void setPrefsLocation(File prefsFile) { this.prefsFile = prefsFile; } File prefsFile; /** * Set location for user preferences file. * * <p>File need not exist, but location must be writable when storePrefs() called. */ /*public void setUserPrefsLocation(File userPrefsFile) { this.userPrefsFile = userPrefsFile; } File userPrefsFile;*/ /** * Writes prefs to a file. * * @param file */ public boolean storeConfig(File file) { boolean result = true; Element root = initStore(); if (!addConfigStore(root)) { result = false; } includeHistory(root); if (!finalStore(root, file)) { result = false; } return result; } /** * Writes user and config info to a file. * * <p>Config is included here because it doesnt hurt to read it again, and the user data * (typically a panel) requires it to be present first. * * @param file */ public boolean storeUser(File file) { boolean result = true; Element root = initStore(); if (!addConfigStore(root)) { result = false; } if (!addUserStore(root)) { result = false; } includeHistory(root); if (!finalStore(root, file)) { result = false; } return result; } public boolean makeBackup(File file) { return makeBackupFile(defaultBackupDirectory, file); } String defaultBackupDirectory = FileUtil.getUserFilesPath() + "backupPanels"; /** * @param o The object to get an XML representation of * @return An XML element representing o * @deprecated */ @Deprecated public static Element elementFromObject(Object o) { return ConfigXmlManager.elementFromObject(o, true); } /** * @param object The object to get an XML representation of * @param shared true if the XML should be shared, false if the XML should be per-node * @return An XML element representing object */ public static Element elementFromObject(Object object, boolean shared) { String aName = adapterName(object); log.debug("store using {}", aName); XmlAdapter adapter = null; try { adapter = (XmlAdapter) Class.forName(adapterName(object)).newInstance(); } catch (java.lang.ClassNotFoundException | java.lang.IllegalAccessException | java.lang.InstantiationException ex) { log.error("Cannot load configuration adapter for {}", object.getClass().getName(), ex); } if (adapter != null) { return adapter.store(object, shared); } else { log.error("Cannot store configuration for {}", object.getClass().getName()); return null; } } private void storeVersion(Element root) { // add version at front root.addContent( 0, new Element("jmriversion") .addContent(new Element("major").addContent("" + jmri.Version.major)) .addContent(new Element("minor").addContent("" + jmri.Version.minor)) .addContent(new Element("test").addContent("" + jmri.Version.test)) .addContent(new Element("modifier").addContent(jmri.Version.getModifier()))); } // private void loadVersion(Element root, XmlAdapter adapter) { // int majorRelease = 0; // int minorRelease = 0; // int testRelease = 0; // Element v = root.getChild("jmriversion"); // if (v!=null) { // try { // majorRelease = Integer.parseInt(v.getChild("major").getText()); // minorRelease = Integer.parseInt(v.getChild("minor").getText()); // testRelease = Integer.parseInt(v.getChild("test").getText()); // } catch (NullPointerException npe) { // } catch ( NumberFormatException nfe) { // } // } // adapter.setConfigXmlManager(this); // adapter.setMajorRelease(majorRelease); // adapter.setMinorRelease(minorRelease); // adapter.setTestRelease(testRelease); // } /** * Load a file. * * <p>Handles problems locally to the extent that it can, by routing them to the * creationErrorEncountered method. * * @return true if no problems during the load */ public boolean load(File fi) throws JmriConfigureXmlException { return load(fi, false); } public boolean load(URL url) throws JmriConfigureXmlException { return load(url, false); } /** * Load a file. * * <p>Handles problems locally to the extent that it can, by routing them to the * creationErrorEncountered method. * * @param fi file to load * @param registerDeferred true to register objects to defer * @return true if no problems during the load * @throws JmriConfigureXmlException * @see jmri.configurexml.XmlAdapter#loadDeferred() * @since 2.11.2 */ @Override public boolean load(File fi, boolean registerDeferred) throws JmriConfigureXmlException { return this.load(FileUtil.fileToURL(fi), registerDeferred); } /** * Load a file. * * <p>Handles problems locally to the extent that it can, by routing them to the * creationErrorEncountered method. * * @param url URL of file to load * @param registerDeferred true to register objects to defer * @return true if no problems during the load * @throws JmriConfigureXmlException * @see jmri.configurexml.XmlAdapter#loadDeferred() * @since 3.3.2 */ @Override public boolean load(URL url, boolean registerDeferred) throws JmriConfigureXmlException { boolean result = true; Element root = null; /* We will put all the elements into a load list, along with the load order As XML files prior to 2.13.1 had no order to the store, beans would be stored/loaded before beans that they were dependant upon had been stored/loaded */ Map<Element, Integer> loadlist = Collections.synchronizedMap(new LinkedHashMap<Element, Integer>()); try { root = super.rootFromURL(url); // get the objects to load List<Element> items = root.getChildren(); for (int i = 0; i < items.size(); i++) { // Put things into an ordered list Element item = items.get(i); if (item.getAttribute("class") == null) { // this is an element that we're not meant to read if (log.isDebugEnabled()) { log.debug("skipping " + item); } continue; } String adapterName = item.getAttribute("class").getValue(); if (log.isDebugEnabled()) { log.debug("attempt to get adapter " + adapterName + " for " + item); } XmlAdapter adapter = null; adapter = (XmlAdapter) Class.forName(adapterName).newInstance(); int order = adapter.loadOrder(); if (log.isDebugEnabled()) { log.debug("add " + item + " to load list with order id of " + order); } loadlist.put(item, order); } ArrayList<Map.Entry<Element, Integer>> l = new ArrayList<Map.Entry<Element, Integer>>(loadlist.entrySet()); Collections.sort( l, new Comparator<Map.Entry<Element, Integer>>() { public int compare(Map.Entry<Element, Integer> o1, Map.Entry<Element, Integer> o2) { return o1.getValue().compareTo(o2.getValue()); } }); for (int i = 0; i < l.size(); i++) { Element item = l.get(i).getKey(); String adapterName = item.getAttribute("class").getValue(); if (log.isDebugEnabled()) { log.debug("load " + item + " via " + adapterName); } XmlAdapter adapter = null; try { adapter = (XmlAdapter) Class.forName(adapterName).newInstance(); // get version info // loadVersion(root, adapter); // and do it if (adapter.loadDeferred() && registerDeferred) { // register in the list for deferred load loadDeferredList.add(item); if (log.isDebugEnabled()) { log.debug("deferred load registered for " + item + " " + adapterName); } } else { boolean loadStatus = adapter.load(item, null); if (log.isDebugEnabled()) { log.debug("load status for " + item + " " + adapterName + " is " + loadStatus); } // if any adaptor load fails, then the entire load has failed if (!loadStatus) { result = false; } } } catch (Exception e) { creationErrorEncountered( adapter, "load(" + url.getFile() + ")", "Unexpected error (Exception)", null, null, e); result = false; // keep going, but return false to signal problem } catch (Throwable et) { creationErrorEncountered( adapter, "in load(" + url.getFile() + ")", "Unexpected error (Throwable)", null, null, et); result = false; // keep going, but return false to signal problem } } } catch (java.io.FileNotFoundException e1) { // this returns false to indicate un-success, but not enough // of an error to require a message creationErrorEncountered( null, "opening file " + url.getFile(), "File not found", null, null, e1); result = false; } catch (org.jdom2.JDOMException e) { creationErrorEncountered(null, "parsing file " + url.getFile(), "Parse error", null, null, e); result = false; } catch (java.lang.Exception e) { creationErrorEncountered( null, "loading from file " + url.getFile(), "Unknown error (Exception)", null, null, e); result = false; } finally { // no matter what, close error reporting handler.done(); } /*try { root = super.rootFromFile(fi); // get the objects to load List<Element> items = root.getChildren(); for (int i = 0; i<items.size(); i++) { // get the class, hence the adapter object to do loading Element item = items.get(i); if (item.getAttribute("class") == null) { // this is an element that we're not meant to read continue; } String adapterName = item.getAttribute("class").getValue(); log.debug("load via "+adapterName); XmlAdapter adapter = null; try { adapter = (XmlAdapter)Class.forName(adapterName).newInstance(); // get version info // loadVersion(root, adapter); // and do it if (adapter.loadDeferred() && registerDeferred) { // register in the list for deferred load loadDeferredList.add(item); log.debug("deferred load registered for " + adapterName); } else { boolean loadStatus = adapter.load(item); log.debug("load status for "+adapterName+" is "+loadStatus); // if any adaptor load fails, then the entire load has failed if (!loadStatus) result = false; } } catch (Exception e) { creationErrorEncountered (adapter, "load("+fi.getName()+")",Level.ERROR, "Unexpected error (Exception)",null,null,e); result = false; // keep going, but return false to signal problem } catch (Throwable et) { creationErrorEncountered (adapter, "in load("+fi.getName()+")", Level.ERROR, "Unexpected error (Throwable)",null,null,et); result = false; // keep going, but return false to signal problem } } } catch (java.io.FileNotFoundException e1) { // this returns false to indicate un-success, but not enough // of an error to require a message creationErrorEncountered (null, "opening file "+fi.getName(), Level.ERROR, "File not found", null,null,e1); result = false; } catch (org.jdom2.JDOMException e) { creationErrorEncountered (null, "parsing file "+fi.getName(), Level.ERROR, "Parse error", null,null,e); result = false; } catch (java.lang.Exception e) { creationErrorEncountered (null, "loading from file "+fi.getName(), Level.ERROR, "Unknown error (Exception)", null,null,e); result = false; } finally { // no matter what, close error reporting handler.done(); }*/ // loading complete, as far as it got, make history entry FileHistory r = InstanceManager.getDefault(FileHistory.class); if (r != null) { FileHistory included = null; if (root != null) { Element filehistory = root.getChild("filehistory"); if (filehistory != null) { included = jmri.jmrit.revhistory.configurexml.FileHistoryXml.loadFileHistory(filehistory); } } r.addOperation((result ? "Load OK" : "Load with errors"), url.getFile(), included); } else { log.info("Not recording file history"); } return result; } @Override public boolean loadDeferred(File fi) { return this.loadDeferred(FileUtil.fileToURL(fi)); } @Override public boolean loadDeferred(URL url) { boolean result = true; // Now process the load-later list log.debug("Start processing deferred load list (size): " + loadDeferredList.size()); if (!loadDeferredList.isEmpty()) { for (Element item : loadDeferredList) { String adapterName = item.getAttribute("class").getValue(); log.debug("deferred load via " + adapterName); XmlAdapter adapter = null; try { adapter = (XmlAdapter) Class.forName(adapterName).newInstance(); boolean loadStatus = adapter.load(item, null); log.debug("deferred load status for " + adapterName + " is " + loadStatus); // if any adaptor load fails, then the entire load has failed if (!loadStatus) { result = false; } } catch (Exception e) { creationErrorEncountered( adapter, "deferred load(" + url.getFile() + ")", "Unexpected error (Exception)", null, null, e); result = false; // keep going, but return false to signal problem } catch (Throwable et) { creationErrorEncountered( adapter, "in deferred load(" + url.getFile() + ")", "Unexpected error (Throwable)", null, null, et); result = false; // keep going, but return false to signal problem } } } log.debug("Done processing deferred load list with result: " + result); return result; } /** * Find a file by looking * * <UL> * <LI>in xml/layout/ in the preferences directory, if that exists * <LI>in xml/layout/ in the application directory, if that exists * <LI>in xml/ in the preferences directory, if that exists * <LI>in xml/ in the application directory, if that exists * <LI>at top level in the application directory * <LI> * </ul> * * @param f Local filename, perhaps without path information * @return Corresponding File object */ @Override public URL find(String f) { URL u = FileUtil.findURL(f, "xml/layout", "xml"); // NOI18N if (u == null) { this.locateFileFailed(f); } return u; } /** * Report a failure to find a file. This is a separate member to ease testing. * * @param f Name of file not located. */ void locateFileFailed(String f) { log.warn("Could not locate file " + f); } /** * Invoke common handling of errors that happen during the "load" process. * * <p>Generally, this is invoked by {@link XmlAdapter} implementations of their * creationErrorEncountered() method (note different arguemments, though). The standard * implemenation of that is in {@link AbstractXmlAdapter}. * * <p>Exceptions passed into this are absorbed. * * @param adapter Object that encountered the error (for reporting), may be null * @param operation description of the operation being attempted, may be null * @param description description of error encountered * @param systemName System name of bean being handled, may be null * @param userName used name of the bean being handled, may be null * @param exception Any exception being handled in the processing, may be null */ public static void creationErrorEncountered( XmlAdapter adapter, String operation, String description, String systemName, String userName, Throwable exception) { // format and log a message (note reordered from arguments) ErrorMemo e = new ErrorMemo(adapter, operation, description, systemName, userName, exception, "loading"); handler.handle(e); } /** * Invoke common handling of errors that happen during the "store" process. * * <p>Generally, this is invoked by {@link XmlAdapter} implementations of their * creationErrorEncountered() method (note different arguemments, though). The standard * implemenation of that is in {@link AbstractXmlAdapter}. * * <p>Exceptions passed into this are absorbed. * * @param adapter Object that encountered the error (for reporting), may be null * @param operation description of the operation being attempted, may be null * @param description description of error encountered * @param systemName System name of bean being handled, may be null * @param userName used name of the bean being handled, may be null * @param exception Any exception being handled in the processing, may be null */ public static void storingErrorEncountered( XmlAdapter adapter, String operation, String description, String systemName, String userName, Throwable exception) { // format and log a message (note reordered from arguments) ErrorMemo e = new ErrorMemo(adapter, operation, description, systemName, userName, exception, "storing"); handler.handle(e); } static ErrorHandler handler = new ErrorHandler(); public static void setErrorHandler(ErrorHandler handler) { ConfigXmlManager.handler = handler; } // initialize logging private static final Logger log = LoggerFactory.getLogger(ConfigXmlManager.class.getName()); /** @return the loadDeferredList */ protected ArrayList<Element> getLoadDeferredList() { return loadDeferredList; } }
@edu.umd.cs.findbugs.annotations.SuppressWarnings({ "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", "SC_START_IN_CTOR" }) // "only one application at a time. The thread is only called to help improve user experiance // when opening the preferences, it is not critical for it to be run at this stage" public Apps(JFrame frame) { super(true); long start = System.nanoTime(); splash(false); splash(true, true); setButtonSpace(); setJynstrumentSpace(); jmri.Application.setLogo(logo()); jmri.Application.setURL(line2()); // Enable proper snapping of JSliders SliderSnap.init(); // Prepare font lists prepareFontLists(); // install shutdown manager InstanceManager.setShutDownManager(new DefaultShutDownManager()); // add the default shutdown task to save blocks // as a special case, register a ShutDownTask to write out blocks InstanceManager.shutDownManagerInstance() .register( new AbstractShutDownTask("Writing Blocks") { @Override public boolean execute() { // Save block values prior to exit, if necessary log.debug("Start writing block info"); try { new BlockValueFile().writeBlockValues(); } // catch (org.jdom2.JDOMException jde) { log.error("Exception writing blocks: {}", // jde); } catch (IOException ioe) { log.error("Exception writing blocks: {}", ioe); } // continue shutdown return true; } }); // Get configuration profile // Needs to be done before loading a ConfigManager or UserPreferencesManager FileUtil.createDirectory(FileUtil.getPreferencesPath()); // Needs to be declared final as we might need to // refer to this on the Swing thread final File profileFile; profileFilename = configFilename.replaceFirst(".xml", ".properties"); // decide whether name is absolute or relative if (!new File(profileFilename).isAbsolute()) { // must be relative, but we want it to // be relative to the preferences directory profileFile = new File(FileUtil.getPreferencesPath() + profileFilename); } else { profileFile = new File(profileFilename); } ProfileManager.getDefault().setConfigFile(profileFile); // See if the profile to use has been specified on the command line as // a system property jmri.profile as a profile id. if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) { ProfileManager.getDefault() .setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY)); } // @see jmri.profile.ProfileManager#migrateToProfiles JavaDoc for conditions handled here if (!ProfileManager.getDefault().getConfigFile().exists()) { // no profile config for this app try { if (ProfileManager.getDefault() .migrateToProfiles(configFilename)) { // migration or first use // notify user of change only if migration occured // TODO: a real migration message JOptionPane.showMessageDialog( sp, Bundle.getMessage("ConfigMigratedToProfile"), jmri.Application.getApplicationName(), JOptionPane.INFORMATION_MESSAGE); } } catch (IOException | IllegalArgumentException ex) { JOptionPane.showMessageDialog( sp, ex.getLocalizedMessage(), jmri.Application.getApplicationName(), JOptionPane.ERROR_MESSAGE); log.error(ex.getMessage()); } } try { ProfileManagerDialog.getStartingProfile(sp); // Manually setting the configFilename property since calling // Apps.setConfigFilename() does not reset the system property configFilename = FileUtil.getProfilePath() + Profile.CONFIG_FILENAME; System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME); log.info("Starting with profile {}", ProfileManager.getDefault().getActiveProfile().getId()); } catch (IOException ex) { log.info( "Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage()); } // Install configuration manager and Swing error handler ConfigureManager cm = new JmriConfigurationManager(); InstanceManager.store(cm, ConfigureManager.class); InstanceManager.setDefault(ConfigureManager.class, cm); // Install a history manager InstanceManager.store(new FileHistory(), FileHistory.class); // record startup InstanceManager.getDefault(FileHistory.class).addOperation("app", nameString, null); // Install a user preferences manager InstanceManager.store( DefaultUserMessagePreferences.getInstance(), UserPreferencesManager.class); InstanceManager.store(new NamedBeanHandleManager(), NamedBeanHandleManager.class); // Install an IdTag manager InstanceManager.store(new DefaultIdTagManager(), IdTagManager.class); // Install Entry Exit Pairs Manager InstanceManager.store(new EntryExitPairs(), EntryExitPairs.class); // install preference manager InstanceManager.store(new TabbedPreferences(), TabbedPreferences.class); // Install abstractActionModel InstanceManager.store(new apps.CreateButtonModel(), apps.CreateButtonModel.class); // find preference file and set location in configuration manager // Needs to be declared final as we might need to // refer to this on the Swing thread final File file; File singleConfig; File sharedConfig = null; // decide whether name is absolute or relative if (!new File(configFilename).isAbsolute()) { // must be relative, but we want it to // be relative to the preferences directory singleConfig = new File(FileUtil.getUserFilesPath() + configFilename); } else { singleConfig = new File(configFilename); } try { // get preferences file sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG); if (!sharedConfig.canRead()) { sharedConfig = null; } } catch (FileNotFoundException ex) { // ignore - sharedConfig will remain null in this case } // load config file if it exists if (sharedConfig != null) { file = sharedConfig; } else { file = singleConfig; } log.debug("Using config file(s) {}", file.getPath()); if (file.exists()) { log.debug("start load config file {}", file.getPath()); try { configOK = InstanceManager.configureManagerInstance().load(file, true); } catch (JmriException e) { log.error("Unhandled problem loading configuration", e); configOK = false; } log.debug("end load config file, OK={}", configOK); } else { log.info( "No saved preferences, will open preferences window. Searched for {}", file.getPath()); configOK = false; } // Add actions to abstractActionModel // Done here as initial non-GUI initialisation is completed // and UI L&F has been set addToActionModel(); // populate GUI log.debug("Start UI"); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); // Create a WindowInterface object based on the passed-in Frame JFrameInterface wi = new JFrameInterface(frame); // Create a menu bar menuBar = new JMenuBar(); // Create menu categories and add to the menu bar, add actions to menus createMenus(menuBar, wi); // done long end = System.nanoTime(); long elapsedTime = (end - start) / 1000000; /* This ensures that the message is displayed on the screen for a minimum of 2.5seconds, if the time taken to get to this point in the code is longer that 2.5seconds then the wait is not invoked. */ long sleep = 2500 - elapsedTime; if (sleep > 0) { log.debug( "Debug message was displayed for less than 2500ms ({}ms). Sleeping for {}ms to allow user sufficient time to do something.", elapsedTime, sleep); try { Thread.sleep(sleep); } catch (InterruptedException e) { log.error(e.getLocalizedMessage(), e); } } FileUtil.logFilePaths(); splash(false); splash(true, false); Toolkit.getDefaultToolkit().removeAWTEventListener(debugListener); while (debugmsg) { /*The user has pressed the interupt key that allows them to disable logixs at start up we do not want to process any more information until the user has answered the question */ try { Thread.sleep(1000); } catch (InterruptedException e) { log.error(e.getLocalizedMessage(), e); } } // Now load deferred config items if (file.exists() && file.equals(singleConfig)) { // To avoid possible locks, deferred load should be // performed on the Swing thread if (SwingUtilities.isEventDispatchThread()) { configDeferredLoadOK = doDeferredLoad(file); } else { try { // Use invokeAndWait method as we don't want to // return until deferred load is completed SwingUtilities.invokeAndWait( new Runnable() { @Override public void run() { configDeferredLoadOK = doDeferredLoad(file); } }); } catch (InterruptedException | InvocationTargetException ex) { log.error("Exception creating system console frame", ex); } } } else { configDeferredLoadOK = false; } // If preferences need to be migrated, do it now if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) { log.info("Migrating preferences to new format..."); // migrate preferences InstanceManager.tabbedPreferencesInstance().init(); InstanceManager.tabbedPreferencesInstance().saveContents(); InstanceManager.configureManagerInstance().storePrefs(); // notify user of change log.info("Preferences have been migrated to new format."); log.info("New preferences format will be used after JMRI is restarted."); if (!GraphicsEnvironment.isHeadless()) { JOptionPane.showMessageDialog( sp, Bundle.getMessage( "SingleConfigMigratedToSharedConfig", ProfileManager.getDefault().getActiveProfile().getName()), jmri.Application.getApplicationName(), JOptionPane.INFORMATION_MESSAGE); } } /*Once all the preferences have been loaded we can initial the preferences doing it in a thread at this stage means we can let it work in the background*/ Runnable r = new Runnable() { @Override public void run() { try { InstanceManager.tabbedPreferencesInstance().init(); } catch (Exception ex) { log.error("Error trying to setup preferences {}", ex.getLocalizedMessage(), ex); } } }; Thread thr = new Thread(r, "initialize preferences"); thr.start(); // Initialise the decoderindex file instance within a seperate thread to help improve first use // perfomance r = new Runnable() { @Override public void run() { try { DecoderIndexFile.instance(); } catch (Exception ex) { log.error("Error in trying to initialize decoder index file {}", ex.toString()); } } }; Thread thr2 = new Thread(r, "initialize decoder index"); thr2.start(); if (Boolean.getBoolean("org.jmri.python.preload")) { r = new Runnable() { public void run() { try { JmriScriptEngineManager.getDefault().initializeAllEngines(); } catch (Exception ex) { log.error("Error in trying to initialize python interpreter {}", ex.toString()); } } }; Thread thr3 = new Thread(r, "initialize python interpreter"); thr3.start(); } // if the configuration didn't complete OK, pop the prefs frame and help log.debug("Config go OK? {}", (configOK || configDeferredLoadOK)); if (!configOK || !configDeferredLoadOK) { HelpUtil.displayHelpRef("package.apps.AppConfigPanelErrorPage"); doPreferences(); } log.debug("Done with doPreferences, start statusPanel"); add(statusPanel()); log.debug("Done with statusPanel, start buttonSpace"); add(buttonSpace()); add(_jynstrumentSpace); long eventMask = AWTEvent.MOUSE_EVENT_MASK; Toolkit.getDefaultToolkit() .addAWTEventListener( new AWTEventListener() { @Override public void eventDispatched(AWTEvent e) { if (e instanceof MouseEvent) { MouseEvent me = (MouseEvent) e; if (me.isPopupTrigger() && me.getComponent() instanceof JTextComponent) { final JTextComponent component = (JTextComponent) me.getComponent(); final JPopupMenu menu = new JPopupMenu(); JMenuItem item; item = new JMenuItem(new DefaultEditorKit.CopyAction()); item.setText("Copy"); item.setEnabled(component.getSelectionStart() != component.getSelectionEnd()); menu.add(item); item = new JMenuItem(new DefaultEditorKit.CutAction()); item.setText("Cut"); item.setEnabled( component.isEditable() && component.getSelectionStart() != component.getSelectionEnd()); menu.add(item); item = new JMenuItem(new DefaultEditorKit.PasteAction()); item.setText("Paste"); item.setEnabled(component.isEditable()); menu.add(item); menu.show(me.getComponent(), me.getX(), me.getY()); } } } }, eventMask); // do final activation InstanceManager.logixManagerInstance().activateAllLogixs(); InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class) .initializeLayoutBlockPaths(); // Loads too late - now started from ItemPalette // new jmri.jmrit.catalog.configurexml.DefaultCatalogTreeManagerXml().readCatalogTrees(); log.debug("End constructor"); }
/** * Roster manages and manipulates a roster of locomotives. * * <p>It works with the "roster-config" XML schema to load and store its information. * * <p>This is an in-memory representation of the roster xml file (see below for constants defining * name and location). As such, this class is also responsible for the "dirty bit" handling to * ensure it gets written. As a temporary reliability enhancement, all changes to this structure are * now being written to a backup file, and a copy is made when the file is opened. * * <p>Multiple Roster objects don't make sense, so we use an "instance" member to navigate to a * single one. * * <p>The only bound property is the list of RosterEntrys; a PropertyChangedEvent is fired every * time that changes. * * <p>The entries are stored in an ArrayList, sorted alphabetically. That sort is done manually each * time an entry is added. * * <p>The roster is stored in a "Roster Index", which can be read or written. Each individual entry * (once stored) contains a filename which can be used to retrieve the locomotive information for * that roster entry. Note that the RosterEntry information is duplicated in both the Roster (stored * in the roster.xml file) and in the specific file for the entry. * * <p>Originally, JMRI managed just one global roster, held in a global Roster object. With the rise * of more complicated layouts, code has been added to address multiple rosters, with the primary * one now held in Roster.default(). We're moving references to Roster.default() out to the using * code, so that eventually we can make those explicit references to other Roster objects as/when * needed. * * @author Bob Jacobsen Copyright (C) 2001, 2008, 2010 * @author Dennis Miller Copyright 2004 * @see jmri.jmrit.roster.RosterEntry */ public class Roster extends XmlFile implements RosterGroupSelector, PropertyChangeProvider, PropertyChangeListener { /** List of contained {@link RosterEntry} elements. */ protected List<RosterEntry> _list = new ArrayList<>(); private boolean dirty = false; /* * This should always be a real path, changes in the UserFiles location are * tracked by listening to FileUtilSupport for those changes and updating * this path as needed. */ private String rosterLocation = FileUtil.getUserFilesPath(); private String rosterIndexFileName = Roster.DEFAULT_ROSTER_INDEX; // since we can't do a "super(this)" in the ctor to inherit from PropertyChangeSupport, we'll // reflect to it. // Note that dispose() doesn't act on these. Its not clear whether it should... private PropertyChangeSupport pcs = new PropertyChangeSupport(this); public static final String schemaVersion = ""; // NOI18N private UserPreferencesManager preferences; private String defaultRosterGroup = null; private final HashMap<String, RosterGroup> rosterGroups = new HashMap<>(); // initialize logging private static final Logger log = LoggerFactory.getLogger(Roster.class); /** Name of the default roster index file. {@value #DEFAULT_ROSTER_INDEX} */ public static final String DEFAULT_ROSTER_INDEX = "roster.xml"; // NOI18N /** Name for the property change fired when adding a roster entry. {@value #ADD} */ public static final String ADD = "add"; // NOI18N /** Name for the property change fired when removing a roster entry. {@value #REMOVE} */ public static final String REMOVE = "remove"; // NOI18N /** Name for the property change fired when changing the ID of a roster entry. {@value #CHANGE} */ public static final String CHANGE = "change"; // NOI18N /** Property change event fired when saving the roster. {@value #SAVED} */ public static final String SAVED = "saved"; // NOI18N /** Property change fired when adding a roster group. {@value #ROSTER_GROUP_ADDED} */ public static final String ROSTER_GROUP_ADDED = "RosterGroupAdded"; // NOI18N /** Property change fired when removing a roster group. {@value #ROSTER_GROUP_REMOVED} */ public static final String ROSTER_GROUP_REMOVED = "RosterGroupRemoved"; // NOI18N /** Property change fired when renaming a roster group. {@value #ROSTER_GROUP_RENAMED} */ public static final String ROSTER_GROUP_RENAMED = "RosterGroupRenamed"; // NOI18N /** * String prefixed to roster group names in the roster entry XML. {@value #ROSTER_GROUP_PREFIX} */ public static final String ROSTER_GROUP_PREFIX = "RosterGroup:"; // NOI18N /** * Title of the "All Entries" roster group. As this varies by locale, do not rely on being able to * store this value. */ public static final String ALLENTRIES = Bundle.getMessage("ALLENTRIES"); // NOI18N /** * Create a default roster. Generally it is preferable to use the Roster returned by {@link * #getDefault()}. */ public Roster() { super(); FileUtilSupport.getDefault() .addPropertyChangeListener( FileUtil.PREFERENCES, (PropertyChangeEvent evt) -> { if (Roster.this.getRosterLocation().equals(evt.getOldValue())) { Roster.this.setRosterLocation((String) evt.getNewValue()); Roster.this.reloadRosterFile(); } }); this.preferences = InstanceManager.getNullableDefault(UserPreferencesManager.class); if (this.preferences != null) { // for some reason, during JUnit testing, preferences is often null this.setDefaultRosterGroup( (String) this.preferences.getProperty( Roster.class.getCanonicalName(), "defaultRosterGroup")); // NOI18N } } // should be private except that JUnit testing creates multiple Roster objects public Roster(String rosterFilename) { this(); try { // if the rosterFilename passed in is null, create a complete path // to the default roster index before attempting to read if (rosterFilename == null) { rosterFilename = this.getRosterIndexPath(); } this.readFile(rosterFilename); } catch (IOException | JDOMException e) { log.error("Exception during roster reading: " + e); try { JOptionPane.showMessageDialog( null, Bundle.getMessage("ErrorReadingText") + "\n" + e.getMessage(), Bundle.getMessage("ErrorReadingTitle"), JOptionPane.ERROR_MESSAGE); } catch (HeadlessException he) { // ignore inability to display dialog } } } /** * Locate the single instance of Roster, loading it if need be. * * <p>Calls {@link #getDefault() } to provide the single instance. * * @deprecated 4.5.1 * @return The valid Roster object */ @Deprecated public static synchronized Roster instance() { return Roster.getDefault(); } /** * Get the default Roster instance, creating it as required. * * @return The default Roster object */ public static synchronized Roster getDefault() { if (InstanceManager.getNullableDefault(Roster.class) == null) { log.debug("Creating Roster default instance."); // Pass null to use defaults. InstanceManager.setDefault(Roster.class, new Roster(null)); } return InstanceManager.getDefault(Roster.class); } /** * Add a RosterEntry object to the in-memory Roster. * * @param e Entry to add */ public void addEntry(RosterEntry e) { if (log.isDebugEnabled()) { log.debug("Add entry " + e); } int i = _list.size() - 1; // Last valid index while (i >= 0) { if (e.getId().compareToIgnoreCase(_list.get(i).getId()) > 0) { break; // I can never remember whether I want break or continue here } i--; } _list.add(i + 1, e); e.addPropertyChangeListener(this); this.addRosterGroups(e.getGroups(this)); setDirty(true); firePropertyChange(ADD, null, e); } /** * Remove a RosterEntry object from the in-memory Roster. This does not delete the file for the * RosterEntry! * * @param e Entry to remove */ public void removeEntry(RosterEntry e) { log.debug("Remove entry {}", e); _list.remove(e); e.removePropertyChangeListener(this); setDirty(true); firePropertyChange(REMOVE, e, null); } /** @return Number of entries in the Roster. */ public int numEntries() { return _list.size(); } /** * @param group The group being queried or null for all entries in the roster. * @return The Number of roster entries in the specified group or 0 if the group does not exist. */ public int numGroupEntries(String group) { if (group != null && !group.equals(Roster.ALLENTRIES) && !group.equals(Roster.AllEntries(Locale.getDefault()))) { return (this.rosterGroups.get(group) != null) ? this.rosterGroups.get(group).getEntries().size() : 0; } else { return this.numEntries(); } } /** * Return RosterEntry from a "title" string, ala selection in matchingComboBox. * * @param title The title for the RosterEntry. * @return The matching RosterEntry or null */ public RosterEntry entryFromTitle(String title) { for (RosterEntry re : _list) { if (re.titleString().equals(title)) { return re; } } return null; } /** * Return RosterEntry from a "id" string. * * @param id The id for the RosterEntry. * @return The matching RosterEntry or null */ public RosterEntry getEntryForId(String id) { for (RosterEntry re : _list) { if (re.getId().equals(id)) { return re; } } return null; } /** * Return a list of RosterEntry which have a particular DCC address. * * @param a The address. * @return a List of matching entries, empty if there are not matches. */ @Nonnull public List<RosterEntry> getEntriesByDccAddress(String a) { return findMatchingEntries( (RosterEntry r) -> { return r.getDccAddress().equals(a); }); } /** * Return a specific entry by index * * @param i The RosterEntry at position i in the roster. * @return The matching RosterEntry */ @Nonnull public RosterEntry getEntry(int i) { return _list.get(i); } /** * Get the Nth RosterEntry in the group * * @param group The group being queried. * @param i The index within the group of the requested entry. * @return The specified entry in the group or null if i is larger than the group, or the group * does not exist. */ public RosterEntry getGroupEntry(String group, int i) { List<RosterEntry> l = matchingList(null, null, null, null, null, null, null); int num = 0; for (RosterEntry r : l) { if (group != null) { if ((r.getAttribute(getRosterGroupProperty(group)) != null) && r.getAttribute(getRosterGroupProperty(group)).equals("yes")) { // NOI18N if (num == i) { return r; } num++; } } else { if (num == i) { return r; } num++; } } return null; } public int getGroupIndex(String group, RosterEntry re) { List<RosterEntry> l = matchingList(null, null, null, null, null, null, null); int num = 0; for (RosterEntry r : l) { if (group != null) { if ((r.getAttribute(getRosterGroupProperty(group)) != null) && r.getAttribute(getRosterGroupProperty(group)).equals("yes")) { // NOI18N if (r == re) { return num; } num++; } } else { if (re == r) { return num; } num++; } } return -1; } /** * Return filename from a "title" string, ala selection in matchingComboBox. * * @param title The title for the entry. * @return The filename for the RosterEntry matching title, or null if no such RosterEntry exists. */ public String fileFromTitle(String title) { RosterEntry r = entryFromTitle(title); if (r != null) { return r.getFileName(); } return null; } public List<RosterEntry> getEntriesWithAttributeKey(String key) { // slow but effective algorithm ArrayList<RosterEntry> result = new ArrayList<>(); java.util.Iterator<RosterEntry> i = _list.iterator(); while (i.hasNext()) { RosterEntry r = i.next(); if (r.getAttribute(key) != null) { result.add(r); } } return result; } public List<RosterEntry> getEntriesWithAttributeKeyValue(String key, String value) { // slow but effective algorithm ArrayList<RosterEntry> result = new ArrayList<>(); java.util.Iterator<RosterEntry> i = _list.iterator(); while (i.hasNext()) { RosterEntry r = i.next(); String v = r.getAttribute(key); if (v != null && v.equals(value)) { result.add(r); } } return result; } public Set<String> getAllAttributeKeys() { // slow but effective algorithm Set<String> result = new TreeSet<>(); java.util.Iterator<RosterEntry> i = _list.iterator(); while (i.hasNext()) { RosterEntry r = i.next(); result.addAll(r.getAttributes()); } return result; } public List<RosterEntry> getEntriesInGroup(String group) { if (group == null || group.equals(Roster.ALLENTRIES) || group.isEmpty()) { return this.matchingList(null, null, null, null, null, null, null); } else { return this.getEntriesWithAttributeKeyValue( Roster.getRosterGroupProperty(group), "yes"); // NOI18N } } /** * Internal interface works with #findMatchingEntries to provide a common search-match-return * capability. */ private interface RosterComparator { public boolean check(RosterEntry r); } /** * Internal method works with #RosterComparator to provide a common search-match-return * capability. */ private List<RosterEntry> findMatchingEntries(RosterComparator c) { List<RosterEntry> l = new ArrayList<>(); for (RosterEntry r : _list) { if (c.check(r)) { l.add(r); } } return l; } /** * Get a List of {@link RosterEntry} objects in Roster matching some information. The list will be * empty if there are no matches. * * @param roadName road name of entry or null for any road name * @param roadNumber road number of entry of null for any number * @param dccAddress address of entry or null for any address * @param mfg manufacturer of entry or null for any manufacturer * @param decoderModel decoder model of entry or null for any model * @param decoderFamily decoder family of entry or null for any family * @param id id of entry or null for any id * @param group group entry is member of or null for any group * @return List of matching RosterEntries or an empty List */ @Nonnull public List<RosterEntry> getEntriesMatchingCriteria( String roadName, String roadNumber, String dccAddress, String mfg, String decoderModel, String decoderFamily, String id, String group) { return findMatchingEntries( (RosterEntry r) -> { return checkEntry( r, roadName, roadNumber, dccAddress, mfg, decoderModel, decoderFamily, id, group); }); } /** * Get a List of {@link RosterEntry} objects in Roster matching some information. The list will be * empty if there are no matches. * * <p>This method calls {@link #getEntriesMatchingCriteria(java.lang.String, java.lang.String, * java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, * java.lang.String) } with a null group. * * @param roadName road name of entry or null for any road name * @param roadNumber road number of entry of null for any number * @param dccAddress address of entry or null for any address * @param mfg manufacturer of entry or null for any manufacturer * @param decoderModel decoder model of entry or null for any model * @param decoderFamily decoder family of entry or null for any family * @param id id of entry or null for any id * @return List of matching RosterEntries or an empty List * @see #getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, * java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String) */ @Nonnull public List<RosterEntry> matchingList( String roadName, String roadNumber, String dccAddress, String mfg, String decoderModel, String decoderFamily, String id) { return this.getEntriesMatchingCriteria( roadName, roadNumber, dccAddress, mfg, decoderModel, decoderFamily, id, null); } /** * Check if an entry is consistent with specific properties. * * <p>A null String argument always matches. Strings are used for convenience in GUI building. * * @param i index in the roster for the RosterEntry * @param roadName road name of entry or null for any road name * @param roadNumber road number of entry of null for any number * @param dccAddress address of entry or null for any address * @param mfg manufacturer of entry or null for any manufacturer * @param decoderModel decoder model of entry or null for any model * @param decoderFamily decoder family of entry or null for any family * @param id id of entry or null for any id * @param group group entry is member of or null for any group * @return true if the entry matches */ public boolean checkEntry( int i, String roadName, String roadNumber, String dccAddress, String mfg, String decoderModel, String decoderFamily, String id, String group) { return this.checkEntry( _list, i, roadName, roadNumber, dccAddress, mfg, decoderModel, decoderFamily, id, group); } /** * Check if an entry is consistent with specific properties. * * <p>A null String argument always matches. Strings are used for convenience in GUI building. * * @param list the list of RosterEntrys being searched * @param i the index of the roster entry in the list * @param roadName road name of entry or null for any road name * @param roadNumber road number of entry of null for any number * @param dccAddress address of entry or null for any address * @param mfg manufacturer of entry or null for any manufacturer * @param decoderModel decoder model of entry or null for any model * @param decoderFamily decoder family of entry or null for any family * @param id id of entry or null for any id * @param group group entry is member of or null for any group * @return True if the entry matches */ public boolean checkEntry( List<RosterEntry> list, int i, String roadName, String roadNumber, String dccAddress, String mfg, String decoderModel, String decoderFamily, String id, String group) { RosterEntry r = list.get(i); return checkEntry( r, roadName, roadNumber, dccAddress, mfg, decoderModel, decoderFamily, id, group); } /** * Check if an entry is consistent with specific properties. * * <p>A null String argument always matches. Strings are used for convenience in GUI building. * * @param r the roster entry being checked * @param roadName road name of entry or null for any road name * @param roadNumber road number of entry of null for any number * @param dccAddress address of entry or null for any address * @param mfg manufacturer of entry or null for any manufacturer * @param decoderModel decoder model of entry or null for any model * @param decoderFamily decoder family of entry or null for any family * @param id id of entry or null for any id * @param group group entry is member of or null for any group * @return True if the entry matches */ public boolean checkEntry( RosterEntry r, String roadName, String roadNumber, String dccAddress, String mfg, String decoderModel, String decoderFamily, String id, String group) { if (id != null && !id.equals(r.getId())) { return false; } if (roadName != null && !roadName.equals(r.getRoadName())) { return false; } if (roadNumber != null && !roadNumber.equals(r.getRoadNumber())) { return false; } if (dccAddress != null && !dccAddress.equals(r.getDccAddress())) { return false; } if (mfg != null && !mfg.equals(r.getMfg())) { return false; } if (decoderModel != null && !decoderModel.equals(r.getDecoderModel())) { return false; } if (decoderFamily != null && !decoderFamily.equals(r.getDecoderFamily())) { return false; } if (group != null && !Roster.ALLENTRIES.equals(group) && (r.getAttribute(Roster.getRosterGroupProperty(group)) == null || !r.getAttribute(Roster.getRosterGroupProperty(group)).equals("yes"))) { // NOI18N return false; } return true; } /** * Write the entire roster to a file. * * <p>Creates a new file with the given name, and then calls writeFile (File) to perform the * actual work. * * @param name Filename for new file, including path info as needed. */ void writeFile(String name) throws java.io.FileNotFoundException, java.io.IOException { if (log.isDebugEnabled()) { log.debug("writeFile " + name); } // This is taken in large part from "Java and XML" page 368 File file = findFile(name); if (file == null) { file = new File(name); } writeFile(file); } /** * Write the entire roster to a file object. This does not do backup; that has to be done * separately. See writeRosterFile() for a public function that finds the default location, does a * backup and then calls this. * * @param file an op */ void writeFile(File file) throws java.io.IOException { // create root element Element root = new Element("roster-config"); // NOI18N root.setAttribute( "noNamespaceSchemaLocation", // NOI18N "http://jmri.org/xml/schema/roster" + schemaVersion + ".xsd", // NOI18N org.jdom2.Namespace.getNamespace( "xsi", // NOI18N "http://www.w3.org/2001/XMLSchema-instance")); // NOI18N Document doc = newDocument(root); // add XSLT processing instruction // <?xml-stylesheet type="text/xsl" href="XSLT/roster.xsl"?> java.util.Map<String, String> m = new java.util.HashMap<>(); m.put("type", "text/xsl"); // NOI18N m.put("href", xsltLocation + "roster2array.xsl"); // NOI18N ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); // NOI18N doc.addContent(0, p); String newLocoString = SymbolicProgBundle.getMessage("LabelNewDecoder"); // Check the Comment and Decoder Comment fields for line breaks and // convert them to a processor directive for storage in XML // Note: this is also done in the LocoFile.java class to do // the same thing in the indidvidual locomotive roster files // Note: these changes have to be undone after writing the file // since the memory version of the roster is being changed to the // file version for writing for (int i = 0; i < numEntries(); i++) { // Extract the RosterEntry at this index and inspect the Comment and // Decoder Comment fields to change any \n characters to <?p?> processor // directives so they can be stored in the xml file and converted // back when the file is read. RosterEntry r = _list.get(i); if (!r.getId().equals(newLocoString)) { String tempComment = r.getComment(); String xmlComment = ""; // transfer tempComment to xmlComment one character at a time, except // when \n is found. In that case, insert <?p?> for (int k = 0; k < tempComment.length(); k++) { if (tempComment.startsWith("\n", k)) { // NOI18N xmlComment = xmlComment + "<?p?>"; // NOI18N } else { xmlComment = xmlComment + tempComment.substring(k, k + 1); } } r.setComment(xmlComment); // Now do the same thing for the decoderComment field String tempDecoderComment = r.getDecoderComment(); String xmlDecoderComment = ""; for (int k = 0; k < tempDecoderComment.length(); k++) { if (tempDecoderComment.startsWith("\n", k)) { // NOI18N xmlDecoderComment = xmlDecoderComment + "<?p?>"; // NOI18N } else { xmlDecoderComment = xmlDecoderComment + tempDecoderComment.substring(k, k + 1); } } r.setDecoderComment(xmlDecoderComment); } else { log.debug("skip unsaved roster entry with default name " + r.getId()); } } // All Comments and Decoder Comment line feeds have been changed to processor directives // add top-level elements Element values = new Element("roster"); // NOI18N root.addContent(values); // add entries for (int i = 0; i < numEntries(); i++) { if (!_list.get(i).getId().equals(newLocoString)) { values.addContent(_list.get(i).store()); } else { log.debug("skip unsaved roster entry with default name " + _list.get(i).getId()); } } if (!this.rosterGroups.isEmpty()) { Element rosterGroup = new Element("rosterGroup"); // NOI18N rosterGroups .keySet() .stream() .forEach( (name) -> { Element group = new Element("group"); // NOI18N if (!name.equals(Roster.ALLENTRIES)) { group.addContent(name); rosterGroup.addContent(group); } }); root.addContent(rosterGroup); } writeXML(file, doc); // Now that the roster has been rewritten in file form we need to // restore the RosterEntry object to its normal \n state for the // Comment and Decoder comment fields, otherwise it can cause problems in // other parts of the program (e.g. in copying a roster) for (int i = 0; i < numEntries(); i++) { RosterEntry r = _list.get(i); if (!r.getId().equals(newLocoString)) { String xmlComment = r.getComment(); String tempComment = ""; for (int k = 0; k < xmlComment.length(); k++) { if (xmlComment.startsWith("<?p?>", k)) { // NOI18N tempComment = tempComment + "\n"; // NOI18N k = k + 4; } else { tempComment = tempComment + xmlComment.substring(k, k + 1); } } r.setComment(tempComment); String xmlDecoderComment = r.getDecoderComment(); String tempDecoderComment = ""; // NOI18N for (int k = 0; k < xmlDecoderComment.length(); k++) { if (xmlDecoderComment.startsWith("<?p?>", k)) { // NOI18N tempDecoderComment = tempDecoderComment + "\n"; // NOI18N k = k + 4; } else { tempDecoderComment = tempDecoderComment + xmlDecoderComment.substring(k, k + 1); } } r.setDecoderComment(tempDecoderComment); } else { log.debug("skip unsaved roster entry with default name " + r.getId()); } } // done - roster now stored, so can't be dirty setDirty(false); firePropertyChange(SAVED, false, true); } /** * Name a valid roster entry filename from an entry name. * * <ul> * <li>Replaces all problematic characters with "_". * <li>Append .xml suffix * </ul> * * Does not check for duplicates. * * @return Filename for RosterEntry * @throws IllegalArgumentException if called with null or empty entry name * @param entry the getId() entry name from the RosterEntry * @see RosterEntry#ensureFilenameExists() * @since 2.1.5 */ public static String makeValidFilename(String entry) { if (entry == null) { throw new IllegalArgumentException("makeValidFilename requires non-null argument"); } if (entry.isEmpty()) { throw new IllegalArgumentException("makeValidFilename requires non-empty argument"); } // name sure there are no bogus chars in name String cleanName = entry.replaceAll("[\\W]", "_"); // remove \W, all non-word (a-zA-Z0-9_) characters // NOI18N // ensure suffix return cleanName + ".xml"; // NOI18N } /** * Read the contents of a roster XML file into this object. * * <p>Note that this does not clear any existing entries. * * @param name filename of roster file */ void readFile(String name) throws org.jdom2.JDOMException, java.io.IOException { // roster exists? if (!(new File(name)).exists()) { log.debug( "no roster file found; this is normal if you haven't put decoders in your roster yet"); return; } // find root Element root = rootFromName(name); if (root == null) { log.error("Roster file exists, but could not be read; roster not available"); return; } // if (log.isDebugEnabled()) XmlFile.dumpElement(root); // decode type, invoke proper processing routine if a decoder file if (root.getChild("roster") != null) { // NOI18N List<Element> l = root.getChild("roster").getChildren("locomotive"); // NOI18N if (log.isDebugEnabled()) { log.debug("readFile sees " + l.size() + " children"); } l.stream() .forEach( (e) -> { addEntry(new RosterEntry(e)); }); // Scan the object to check the Comment and Decoder Comment fields for // any <?p?> processor directives and change them to back \n characters for (int i = 0; i < numEntries(); i++) { // Get a RosterEntry object for this index RosterEntry r = _list.get(i); // Extract the Comment field and create a new string for output String tempComment = r.getComment(); String xmlComment = ""; // transfer tempComment to xmlComment one character at a time, except // when <?p?> is found. In that case, insert a \n and skip over those // characters in tempComment. for (int k = 0; k < tempComment.length(); k++) { if (tempComment.startsWith("<?p?>", k)) { // NOI18N xmlComment = xmlComment + "\n"; // NOI18N k = k + 4; } else { xmlComment = xmlComment + tempComment.substring(k, k + 1); } } r.setComment(xmlComment); // Now do the same thing for the decoderComment field String tempDecoderComment = r.getDecoderComment(); String xmlDecoderComment = ""; for (int k = 0; k < tempDecoderComment.length(); k++) { if (tempDecoderComment.startsWith("<?p?>", k)) { // NOI18N xmlDecoderComment = xmlDecoderComment + "\n"; // NOI18N k = k + 4; } else { xmlDecoderComment = xmlDecoderComment + tempDecoderComment.substring(k, k + 1); } } r.setDecoderComment(xmlDecoderComment); } } else { log.error("Unrecognized roster file contents in file: " + name); } if (root.getChild("rosterGroup") != null) { // NOI18N List<Element> groups = root.getChild("rosterGroup").getChildren("group"); // NOI18N groups .stream() .forEach( (group) -> { addRosterGroup(group.getText()); }); } } void setDirty(boolean b) { dirty = b; } boolean isDirty() { return dirty; } public void dispose() { if (log.isDebugEnabled()) { log.debug("dispose"); } if (dirty) { log.error("Dispose invoked on dirty Roster"); } } /** * Store the roster in the default place, including making a backup if needed. * * <p>Uses writeFile(String), a protected method that can write to a specific location. * * @deprecated Since 4.0 Use Roster.getDefault().writeRoster() instead * @see #writeRoster() */ @Deprecated public static void writeRosterFile() { Roster.getDefault().writeRoster(); } /** * Store the roster in the default place, including making a backup if needed. * * <p>Uses writeFile(String), a protected method that can write to a specific location. */ public void writeRoster() { this.makeBackupFile(this.getRosterIndexPath()); try { this.writeFile(this.getRosterIndexPath()); } catch (IOException e) { log.error("Exception while writing the new roster file, may not be complete: {}", e); try { JOptionPane.showMessageDialog( null, Bundle.getMessage("ErrorSavingText") + "\n" + e.getMessage(), Bundle.getMessage("ErrorSavingTitle"), JOptionPane.ERROR_MESSAGE); } catch (HeadlessException he) { // silently ignore failure to display dialog } } } /** Rebuild the Roster index and store it. */ public void reindex() { Roster roster = new Roster(); for (String fileName : Roster.getAllFileNames()) { // Read file try { Element loco = (new LocoFile()) .rootFromName(LocoFile.getFileLocation() + fileName) .getChild("locomotive"); if (loco != null) { RosterEntry re = new RosterEntry(loco); re.setFileName(fileName); roster.addEntry(re); } } catch (JDOMException | IOException ex) { log.error("Exception while loading loco XML file: {} execption: {}", fileName, ex); } } this.makeBackupFile(this.getRosterIndexPath()); try { roster.writeFile(this.getRosterIndexPath()); } catch (IOException ex) { log.error("Exception while writing the new roster file, may not be complete: {}", ex); } this.reloadRosterFile(); log.info("Roster rebuilt, stored in {}", this.getRosterIndexPath()); } /** * Update the in-memory Roster to be consistent with the current roster file. This removes any * existing roster entries! */ public void reloadRosterFile() { // clear existing _list.clear(); this.rosterGroups.clear(); // and read new try { this.readFile(this.getRosterIndexPath()); } catch (IOException | JDOMException e) { log.error("Exception during roster reading: " + e); } } public void setRosterIndexFileName(String fileName) { this.rosterIndexFileName = fileName; } public String getRosterIndexFileName() { return this.rosterIndexFileName; } public String getRosterIndexPath() { return this.getRosterLocation() + this.getRosterIndexFileName(); } /** * Set the default location for the Roster file, and all individual locomotive files. * * @param f Absolute pathname to use. A null or "" argument flags a return to the original default * in the user's files directory. This parameter must be a potentially valid path on the * system. */ public void setRosterLocation(String f) { String oldRosterLocation = this.rosterLocation; String p = f; if (p != null) { if (p.isEmpty()) { p = null; } else { p = FileUtil.getAbsoluteFilename(p); if (p == null) { throw new IllegalArgumentException( Bundle.getMessage("IllegalRosterLocation", f)); // NOI18N } if (!p.endsWith(File.separator)) { p = p + File.separator; } } } if (p == null) { p = FileUtil.getUserFilesPath(); } this.rosterLocation = p; log.debug("Setting roster location from {} to {}", oldRosterLocation, this.rosterLocation); if (this.rosterLocation.equals(FileUtil.getUserFilesPath())) { log.debug("Roster location reset to default"); } if (!this.rosterLocation.equals(oldRosterLocation)) { this.firePropertyChange( RosterConfigManager.DIRECTORY, oldRosterLocation, this.rosterLocation); } this.reloadRosterFile(); } /** * Absolute path to roster file location. * * <p>Default is in the user's files directory, but can be set to anything. * * @return location of the Roster file * @see jmri.util.FileUtil#getUserFilesPath() */ @Nonnull public String getRosterLocation() { return this.rosterLocation; } @Override public synchronized void addPropertyChangeListener(PropertyChangeListener l) { pcs.addPropertyChangeListener(l); } @Override public synchronized void addPropertyChangeListener( String propertyName, PropertyChangeListener listener) { pcs.addPropertyChangeListener(propertyName, listener); } protected void firePropertyChange(String p, Object old, Object n) { pcs.firePropertyChange(p, old, n); } @Override public synchronized void removePropertyChangeListener(PropertyChangeListener l) { pcs.removePropertyChangeListener(l); } @Override public synchronized void removePropertyChangeListener( String propertyName, PropertyChangeListener listener) { pcs.removePropertyChangeListener(propertyName, listener); } @Override public PropertyChangeListener[] getPropertyChangeListeners() { return pcs.getPropertyChangeListeners(); } @Override public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { return pcs.getPropertyChangeListeners(propertyName); } /** * Notify that the ID of an entry has changed. This doesn't actually change the Roster per se, but * triggers recreation. * * @param r The RosterEntry that has changed. */ public void entryIdChanged(RosterEntry r) { log.debug("EntryIdChanged"); // order may be wrong! Sort RosterEntry[] rarray = new RosterEntry[_list.size()]; for (int i = 0; i < rarray.length; i++) { rarray[i] = _list.get(i); } StringUtil.sortUpperCase(rarray); for (int i = 0; i < rarray.length; i++) { _list.set(i, rarray[i]); } firePropertyChange(CHANGE, null, r); } public static String getRosterGroupName(String rosterGroup) { if (rosterGroup == null) { return ALLENTRIES; } return rosterGroup; } /** * Get the string for a RosterGroup property in a RosterEntry * * @param name The name of the rosterGroup * @return The full property string */ public static String getRosterGroupProperty(String name) { return ROSTER_GROUP_PREFIX + name; } /** * Returns the constant used to denote a roster group as a {@link jmri.jmrit.roster.RosterEntry} * attribute. * * @return the value of {@link #ROSTER_GROUP_PREFIX} * @deprecated since 3.11.7 use {@link #ROSTER_GROUP_PREFIX} instead. */ @Deprecated public String getRosterGroupPrefix() { return ROSTER_GROUP_PREFIX; } /** * Add a roster group, notifying all listeners of the change. * * <p>This method fires the property change notification {@value #ROSTER_GROUP_ADDED}. * * @param rg The group to be added */ public void addRosterGroup(RosterGroup rg) { if (this.rosterGroups.containsKey(rg.getName())) { return; } this.rosterGroups.put(rg.getName(), rg); firePropertyChange(ROSTER_GROUP_ADDED, null, rg.getName()); } /** * Add a roster group, notifying all listeners of the change. * * <p>This method creates a {@link jmri.jmrit.roster.rostergroup.RosterGroup}. Use {@link * #addRosterGroup(jmri.jmrit.roster.rostergroup.RosterGroup) } if you need to add a subclass of * RosterGroup. This method fires the property change notification {@value #ROSTER_GROUP_ADDED}. * * @param rg The group to be added */ public void addRosterGroup(String rg) { // do a quick return without creating a new RosterGroup object // if the roster group aleady exists if (this.rosterGroups.containsKey(rg)) { return; } this.addRosterGroup(new RosterGroup(rg)); } /** * Add a list of {@link jmri.jmrit.roster.rostergroup.RosterGroup}. RosterGroups that are already * known to the Roster are ignored. * * @param groups RosterGroups to add to the roster. RosterGroups already in the roster will not be * added again. */ public void addRosterGroups(List<RosterGroup> groups) { groups .stream() .forEach( (rg) -> { this.addRosterGroup(rg); }); } public void removeRosterGroup(RosterGroup rg) { this.delRosterGroupList(rg.getName()); } /** * Delete a roster group, notifying all listeners of the change. * * <p>This method fires the property change notification "{@value #ROSTER_GROUP_REMOVED}". * * @param rg The group to be deleted */ public void delRosterGroupList(String rg) { RosterGroup group = this.rosterGroups.remove(rg); String str = Roster.getRosterGroupProperty(rg); group .getEntries() .stream() .forEach( (re) -> { re.deleteAttribute(str); }); firePropertyChange(ROSTER_GROUP_REMOVED, rg, null); } /** * Copy a roster group, adding every entry in the roster group to the new group. * * <p>If a roster group with the target name already exists, this method silently fails to rename * the roster group. The GUI method CopyRosterGroupAction.performAction() catches this error and * informs the user. This method fires the property change "{@value #ROSTER_GROUP_ADDED}". * * @param oldName Name of the roster group to be copied * @param newName Name of the new roster group * @see jmri.jmrit.roster.swing.RenameRosterGroupAction */ public void copyRosterGroupList(String oldName, String newName) { if (this.rosterGroups.containsKey(newName)) { return; } this.rosterGroups.put(newName, new RosterGroup(newName)); String newGroup = Roster.getRosterGroupProperty(newName); this.rosterGroups .get(oldName) .getEntries() .stream() .forEach( (re) -> { re.putAttribute(newGroup, "yes"); // NOI18N }); this.addRosterGroup(new RosterGroup(newName)); } public void rosterGroupRenamed(String oldName, String newName) { this.firePropertyChange(Roster.ROSTER_GROUP_RENAMED, oldName, newName); } /** * Rename a roster group, while keeping every entry in the roster group. * * <p>If a roster group with the target name already exists, this method silently fails to rename * the roster group. The GUI method RenameRosterGroupAction.performAction() catches this error and * informs the user. This method fires the property change "{@value #ROSTER_GROUP_RENAMED}". * * @param oldName Name of the roster group to be renamed * @param newName New name for the roster group * @see jmri.jmrit.roster.swing.RenameRosterGroupAction */ public void renameRosterGroupList(String oldName, String newName) { if (this.rosterGroups.containsKey(newName)) { return; } this.rosterGroups.get(oldName).setName(newName); } /** * Get a list of the user defined roster group names. * * <p>Strings are immutable, so deleting an item from the copy should not affect the system-wide * list of roster groups. * * @return A list of the roster group names. */ public ArrayList<String> getRosterGroupList() { ArrayList<String> list = new ArrayList<>(this.rosterGroups.keySet()); Collections.sort(list); return list; } /** * Get the identifier for all entries in the roster. * * @param locale - The desired locale * @return "All Entries" in the specified locale */ public static String AllEntries(Locale locale) { return Bundle.getMessage(locale, "ALLENTRIES"); // NOI18N } /** * Get the default roster group. * * <p>This method ensures adherence to the RosterGroupSelector protocol * * @return The entire roster */ @Override public String getSelectedRosterGroup() { return getDefaultRosterGroup(); } /** @return the defaultRosterGroup */ public String getDefaultRosterGroup() { return defaultRosterGroup; } /** @param defaultRosterGroup the defaultRosterGroup to set */ public void setDefaultRosterGroup(String defaultRosterGroup) { this.defaultRosterGroup = defaultRosterGroup; this.preferences.setProperty( Roster.class.getCanonicalName(), "defaultRosterGroup", defaultRosterGroup); // NOI18N } /** Get an array of all the RosterEntry-containing files in the target directory */ static String[] getAllFileNames() { // ensure preferences will be found for read FileUtil.createDirectory(LocoFile.getFileLocation()); // create an array of file names from roster dir in preferences, count entries int i; int np = 0; String[] sp = null; if (log.isDebugEnabled()) { log.debug("search directory " + LocoFile.getFileLocation()); } File fp = new File(LocoFile.getFileLocation()); if (fp.exists()) { sp = fp.list(); if (sp != null) { for (i = 0; i < sp.length; i++) { if (sp[i].endsWith(".xml") || sp[i].endsWith(".XML")) { np++; } } } else { log.warn("expected directory, but {} was a file", LocoFile.getFileLocation()); } } else { log.warn( FileUtil.getUserFilesPath() + "roster directory was missing, though tried to create it"); } // Copy the entries to the final array String sbox[] = new String[np]; int n = 0; if (sp != null && np > 0) { for (i = 0; i < sp.length; i++) { if (sp[i].endsWith(".xml") || sp[i].endsWith(".XML")) { sbox[n++] = sp[i]; } } } // The resulting array is now sorted on file-name to make it easier // for humans to read jmri.util.StringUtil.sort(sbox); if (log.isDebugEnabled()) { log.debug("filename list:"); for (i = 0; i < sbox.length; i++) { log.debug(" " + sbox[i]); } } return sbox; } /** * Get the groups known to the roster itself. Note that changes to the returned Map will not be * reflected in the Roster. * * @return the rosterGroups */ public HashMap<String, RosterGroup> getRosterGroups() { return new HashMap<>(rosterGroups); } /** * Changes the key used to lookup a RosterGroup by name. This is a helper method that does not * fire a notification to any propertyChangeListeners. * * <p>To rename a RosterGroup, use {@link * jmri.jmrit.roster.rostergroup.RosterGroup#setName(java.lang.String)}. * * @param group The group being associated with newKey and will be disassociated with the key * matching {@link RosterGroup#getName()}. * @param newKey The new key by which group can be found in the map of RosterGroups. This should * match the intended new name of group. */ public void remapRosterGroup(RosterGroup group, String newKey) { this.rosterGroups.remove(group.getName()); this.rosterGroups.put(newKey, group); } @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() instanceof RosterEntry) { if (evt.getPropertyName().equals(RosterEntry.ID)) { this.entryIdChanged((RosterEntry) evt.getSource()); } } } }