PropertiesWindow creating duplicates in buildFile (13146)

Okay, Yan, we might need your Java Kung Fu here!

While I’ve been working on a “Chess Clocks” class, I’ve finally put my finger on a bug I’d been “noticing out of the corner of my eye” but figured I was doing something wrong. But actually today I finally dug in and I think it’s a Vassal subtlety that impacts custom classes.

The routine PropertiesWindow.initialize() is experiencing (and improperly ignoring) ClassNotFoundException’s when culling child classes from the “originalState” of the class being edited – resulting in duplicate copies of entries for all child classes that experience the exception. The exception occurs, ironically, in the CANCEL path – in other words to reproduce the bug, you open a parent class’ properties, hit Cancel, and then save the module, and it results in duplication of any child class that is a custom class, because the routine that is supposed to “cull” the child class entries from the one being edited is experiencing and ignoring a ClassNotFoundException.

The following code (in VASSAL.configure.PropertiesWindow) is called whenever the properties for a class (e.g. “Map”) are opened in the Editor. The exception is being thrown when Class.forName gets called, if the class in question is a “custom class”. Somehow custom classes are not on the “classpath” for this method even though the module and editor themselves are able to load the custom classes. So the FIX would be figuring out how to get forName to include any loaded custom classes in its classpath. (OR the more brute force fix would be NOT to ignore the no-such-class and “cull the child entries anyway”).

  protected void initialize(final Configurable target, HelpWindow helpWindow) {
    this.target = target;
    originalState = target.getBuildElement(Builder.createNewDocument());
    Node child = originalState.getFirstChild();
    while (child != null) {
      Node nextChild = child.getNextSibling();
      if (Node.ELEMENT_NODE == child.getNodeType()) {
        // Cull Buildables from the state.
        try {
          final Class<?> c = Class.forName(((Element)child).getTagName());
          if (Buildable.class.isAssignableFrom(c)) {
            originalState.removeChild(child);
          }
        }
        catch (ClassNotFoundException e) {
          // This element doesn't correspond to a class. Skip it.
        }
        catch (ExceptionInInitializerError e) {
          ErrorDialog.bug(e);
        }
        catch (LinkageError e) {
          ErrorDialog.bug(e);
        }
      }
      child = nextChild;
    }
    .
    .
    .

The exception is happening for all custom classes – somehow they are not on the “classpath” used by the Class.forName method used here.

REPRO Steps (100% repro)

  1. Edit any module that has custom classes which are “child classes” (commonly, of Map, e.g. PieceMover or KeyBufferer). An example module is For The People 3.2. Size of module doesn’t really matter this is a “simple” bug.
  2. Open the properties for the parent class (in this case Map) – double click on its entry.
  3. Hit the CANCEL button.
  4. Save the module.
  5. Now examine the module’s buildfile, looking for entries for the child custom class(es)

Expected: One entry for each custom class

Actual: Duplicate entries for each custom class (and for every time you open Map properties and cancel, you get another duplicate copy).

(Oh, and I did post a condensed version of this to the bug tracker – vassalengine.org/tracker/sho … i?id=13146)

Our normal DataArchive.loadClass diverts to findLoadedClass() in the similar exception. Probably doing it here might work for us?

  public synchronized Class<?> loadClass(String name, boolean resolve)
                                         throws ClassNotFoundException {
// FIXME: why is this method this way?
    Class<?> c;
    try {
//      c = findSystemClass(name);
      c = Class.forName(name);
    }
    catch (ClassNotFoundException e) {
      c = findLoadedClass(name);
    }

The default classloaders cannot load custom classes at runtime, this needs a custom classloader.

I wonder how custom classes are loaded elsewhere, the only custom classloader I find is the DataArchive class, which is yet another god class that does everything. Probably it’s classloader part should be refactored into its own class and then used in this method of PropertiesWindow.

On it. I think if instead of Class.forName(name) we call DataArchive.loadclass(name, false), that everything will start working right.

Yes this should work, or something like this. The PropertiesWindow works on another level if I understand correctly, it handles single elements of the buildfile / xml file, while the DataArchive needs a reference to the whole zip file of the module.

Works perfectly. I’ll get something packaged up and into a PR “just like you taught me how to do” :smiley:

PR 60 should fix this github.com/vassalengine/vassal/pull/60

Although I presently can’t build from the master branch (per other message posted in this forum), it works great in my old pre-maven project, and tested correctly with my modules. And the PR passes all the online checks.