コード例 #1
0
ファイル: Roster.java プロジェクト: KenC57/JMRI
 /**
  * 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();
 }
コード例 #2
0
ファイル: Roster.java プロジェクト: KenC57/JMRI
  /** 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;
  }
コード例 #3
0
ファイル: DecoderIndexBuilder.java プロジェクト: KenC57/JMRI
  // 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);
  }
コード例 #4
0
/**
 * 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;
  }
}
コード例 #5
0
ファイル: Apps.java プロジェクト: dpharris/JMRI
  @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");
  }
コード例 #6
0
ファイル: Roster.java プロジェクト: KenC57/JMRI
/**
 * 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());
      }
    }
  }
}