Constructing an instance of a class with SnakeYAML

Discussion in 'Plugin Development' started by Whalum, Feb 12, 2011.

Thread Status:
Not open for further replies.
  1. Offline

    Whalum

    I'm trying to create an instance of a class straight from YAML using SnakeYAML, but it's not happening.

    Here's the error message:
    Show Spoiler

    Code:
    12.2.2011 19:20:01 org.bukkit.craftbukkit.CraftServer loadPlugins
    SEVERE: null; Can't construct a java object for tag:yaml.org,2002:com.bukkit.whalum.casino.Dice; exception=Class not found: com.bukkit.whalum.casino.Dice (Is it up to date?)
    Can't construct a java object for tag:yaml.org,2002:com.bukkit.whalum.casino.Dice; exception=Class not found: com.bukkit.whalum.casino.Dice
     in "<reader>", line 1, column 7:
        name: !!com.bukkit.whalum.casino.Dice  ...
              ^
    
            at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:326)
            at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:184)
            at org.yaml.snakeyaml.constructor.BaseConstructor.constructMapping2ndStep(BaseConstructor.java:327)
            at org.yaml.snakeyaml.constructor.SafeConstructor.constructMapping2ndStep(SafeConstructor.java:125)
            at org.yaml.snakeyaml.constructor.BaseConstructor.constructMapping(BaseConstructor.java:308)
            at org.yaml.snakeyaml.constructor.SafeConstructor$ConstructYamlMap.construct(SafeConstructor.java:443)
            at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:184)
            at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:143)
            at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:129)
            at org.yaml.snakeyaml.Yaml.load(Yaml.java:264)
            at com.bukkit.whalum.casino.Casino.loadGames(Casino.java:109)
            at com.bukkit.whalum.casino.Casino.onEnable(Casino.java:68)
            at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:135)
            at org.bukkit.plugin.java.JavaPluginLoader.enablePlugin(JavaPluginLoader.java:394)
            at org.bukkit.plugin.SimplePluginManager.enablePlugin(SimplePluginManager.java:175)
            at org.bukkit.craftbukkit.CraftServer.loadPlugin(CraftServer.java:69)
            at org.bukkit.craftbukkit.CraftServer.loadPlugins(CraftServer.java:50)
            at net.minecraft.server.MinecraftServer.e(MinecraftServer.java:167)
            at net.minecraft.server.MinecraftServer.c(MinecraftServer.java:154)
            at net.minecraft.server.MinecraftServer.d(MinecraftServer.java:106)
            at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:202)
            at net.minecraft.server.ThreadServerApplication.run(SourceFile:512)
    Caused by: org.yaml.snakeyaml.error.YAMLException: Class not found: com.bukkit.whalum.casino.Dice
            at org.yaml.snakeyaml.constructor.Constructor.getClassForNode(Constructor.java:626)
            at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.getConstructor(Constructor.java:314)
            at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:324)
            ... 21 more


    I can dump to YAML just fine, but loading from it doesn't work.

    The functions I'm using to load from and dump to YAML:
    Show Spoiler

    Code:
    private void loadGames() {
    
           Yaml yaml = new Yaml();
           try {
               HashMap<String, Dice> obj = (HashMap<String, Dice>) yaml.load(new FileReader("output.yml"));
           } catch (FileNotFoundException e) {
               e.printStackTrace();
           }
     }
    
     public void saveGames() {
    
           Yaml yaml = new Yaml();
           FileWriter writer;
           try {
               writer = new FileWriter("output.yml");
               yaml.dump(data, writer);
               writer.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
           System.out.println("Save Done!");
     }


    Dice.java:
    Show Spoiler

    Code:
    package com.bukkit.whalum.casino;
    
    public class Dice {
       private int a;
       private int b;
    
       public Dice() {
    
       }
    
       public int getA() {
          return a;
       }
    
       public int getB() {
          return b;
       }
    
       public void setA(int a) {
          this.a = a;
       }
    
       public void setB(int b) {
          this.b = b;
       }
    
    }
    


    And finally the dumped YAML (created using SnakeYAML):
    Show Spoiler

    Code:
    test: !!com.bukkit.whalum.casino.Dice {a: 3, b: 6}


    All of the above works if I use it outside of Bukkit. But once I try to incorporate this to my plugin all goes wrong. Is there something in Bukkit that prevents this approach?
     
  2. Offline

    Jerry Larsson

    I couldn't get that to work either. Instead I use the built-in configuration class and saved each value manually. instead of dumping the whole class. It was a lot easier.
     
  3. Offline

    frelling

    Edit: See below (2/10/12) for a simpler solution.

    I realize this is an older thread, but it seems to be the only one I could find mentioning this specific problem. As with the previous posters I've experience the same issues. Works great in a standalone app, but results in a ClassNotFound exception when used within a plugin.

    I've used a number of Java YAML libraries over the years. I particularly like the automatic class creation of snakeyaml; thus, when I got around to testing a simple Bukkit plugin, it was a natural choice. Alas, I encountered the same problem as others have experienced.

    The problem is because of the dual dependency between the main craftbukkit JAR and the plugin. A plugin is dependent on craftbukkit. Using snakeyaml (also in the big jar) to automatically construct classes just from class references make it dependent on the plugin.

    A better solution would be to put all support libraries (i.e. json, sqlite, snakeyaml, etc.) into a standalone JAR rather than including them in craftbukkit. This eliminates any potential dual-dependency issues between plugins.

    There are some workarounds for this issue, but none are too pretty. A separate snakeyaml distro could be included in the plugin, although that will require some MANIFEST.MF tweaking to avoid class name collisions. Another option is to refactor the snakeyaml source. It is also possible to create your own Construct classes and inject a replacement for ConstructYamlObject (the source of the problem). Unfortunately, that's also a lot of work and defeats the nice and tidy nature of the snakeyaml package.

    Apart from that, one has to be content with the intrinsic data types and a little extra parsing and conversion. Again, these seems to dullen the capabilities of having snakeyaml, but its the lesser of all evils for the moment.

    Best,

    Frelling

    While I was writing the above post, I had an inspirational moment to test one last approach. It helps when one has spent a couple of hours pouring over snakeyaml source code. :'( This seems to be the cleanest way to get snakeyaml to read in a custom class structure.

    The sample YAML configuration file consists of three mapped entries. The first two entries contain lists of "devices," while the latter entry is a multi-level map. The configuration file is as follows:

    Code:
    allowed_devices:
      - Device 1
      - Device 2
      - Device 3
    
    excluded_device:
      - Device 3
    
    device_info:
      Device1:
        description: This is device 1
        options:
           F: Do something funny
           D: Don’t do something funny
      Device2:
         Description: A device like Device 1 but not as much fun and no options
    The corresponding Java classes are:

    Code:
    package com.di.testplugin.prefs;
    
    import java.util.*;
    
    public class MyPreferences {
      public List<String> allowed_devices;
      public List<String> excluded_devices;
      public Map<String,DeviceInfo> device_info;
    }
    Code:
    package com.di.testplugin.prefs;
    
    import java.util.*;
    
    public class DeviceInfo {
      public String description;
      public Map<String,String> options;
    }
    Next is the code snippet that would be used to set up the YAML descriptions specifying that the MyPreferences class is the root of the YAML document and that DeviceInfo is a subrecord of the device_info field.

    Code:
    MyYamlConstructor cstr = new MyYamlConstructor( MyPreferences.class );
    TypeDescription pDesc = new TypeDescription( MyPreferences.class );
    pDesc.putListProperty( "device_info", DeviceInfo.class );
    cstr.addTypeDescription( pDesc );
    Yaml yaml = new Yaml( cstr );
    ...
    [open file handle to preference file ]
    ...
    try {
       prefs = (MyPreferences) yaml.load( pFile.openStream() );
    }
    catch ( IOException ex ) {
    ...
    }
    Up to this point, none of it is any different than what one would need to do in a regular application that reads in records. The only exception is that instead of using the Constructor class in the description definition above, a derived class is used - MyYamlConstructor - the code for which follows:

    Code:
    package com.di.testplugin.prefs;
    
    import java.util.HashMap;
    
    import org.yaml.snakeyaml.constructor.Constructor;
    import org.yaml.snakeyaml.error.YAMLException;
    import org.yaml.snakeyaml.nodes.Node;
    
    public class MyYamlConstructor extends Constructor {
        private HashMap<String,Class<?>> classMap = new HashMap<String,Class<?>>();
    
           public MyYamlConstructor(Class<? extends Object> theRoot) {
               super( theRoot );
               classMap.put( MyPreferences.class.getName(), MyPreferences.class );
               classMap.put( DeviceInfo.class.getName(), DeviceInfo.class );
           }
    
           /*
            * This is a modified version of the Constructor. Rather than using a class loader to
            * get external classes, they are already predefined above. This approach works similar to
            * the typeTags structure in the original constructor, except that class information is
            * pre-populated during initialization rather than runtime.
            *
            * @see org.yaml.snakeyaml.constructor.Constructor#getClassForNode(org.yaml.snakeyaml.nodes.Node)
            */
            protected Class<?> getClassForNode(Node node) {
                String name = node.getTag().getClassName();
                Class<?> cl = classMap.get( name );
                if ( cl == null )
                    throw new YAMLException( "Class not found: " + name );
                else
                    return cl;
            }
    }
    In essence, the custom class works around the class loader problem by maintaining its own lookup table of classes. snakeyaml also does this by creating a cached list during execution, whereas this class pre-populates the cache with the correct information.

    In sum, this is the cleanest approach that I can conjure up at this time. It does not require any more coding than using snakeyaml to read in custom classes in a regular application, except for the use of a derived version of the Constructor class.

    In the long run, the better solution would still be to segregate support libraries into their own JAR collection rather than bundling them with the craftbukkit JAR.

    I hope this post helps someone. It certainly would have helped me about 10 hours ago. :p

    Best regards,

    Frelling

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 8, 2016
  4. Offline

    frelling

    I should note that the above example creates a TypeDescription that provides additional information about the device_info field for the MyPreferences class because the value object is not a core Java type. This is only required if the field is defined as an interface such as as in this case - Map. If the field is defined as a concrete object - HashMap, LinkedHashMap, etc. - the additional type description is not needed as snakeyaml is capable of automatically determining the appropriate class. However, the main theme of this discussion still stands, that the modified constructor needs to be used to pre-populate class information due to the dual dependencies.

    Also, the custom Constructor can be modified to make it a bit more objectified. Instead of adding class information in the object's constructor, a separate method can be defined as in the following example:

    Code:
    package com.di.testplugin.prefs;
    
    import java.util.HashMap;
    
    import org.yaml.snakeyaml.constructor.Constructor;
    import org.yaml.snakeyaml.error.YAMLException;
    import org.yaml.snakeyaml.nodes.Node;
    
    public class MyYamlConstructor extends Constructor {
        private HashMap<String,Class<?>> classMap = new HashMap<String,Class<?>>();
    
           public MyYamlConstructor(Class<? extends Object> theRoot) {
               super( theRoot );
    
          public void addClassInfo( Class<? extends Object> c ) {
               classMap.put( c.getName(), c );
          }
    
           /*
            * This is a modified version of the Constructor. Rather than using a class loader to
            * get external classes, they are already predefined above. This approach works similar to
            * the typeTags structure in the original constructor, except that class information is
            * pre-populated during initialization rather than runtime.
            *
            * @see org.yaml.snakeyaml.constructor.Constructor#getClassForNode(org.yaml.snakeyaml.nodes.Node)
            */
            protected Class<?> getClassForNode(Node node) {
                String name = node.getTag().getClassName();
                Class<?> cl = classMap.get( name );
                if ( cl == null )
                    throw new YAMLException( "Class not found: " + name );
                else
                    return cl;
            }
    }
    Simple call addClassInfo() as needed after the constructor is instantiated.
     
  5. Offline

    jrw

    This is awesome. Thanks for taking the time to figure this out and post your workaround.
     
  6. Offline

    Hafnium

    Sorry for the necropost, but I spent three hours trying to fix this today and I found a much more elegant solution.

    jrw frelling Whalum


    The problem is that the main thread's (and by extension SnakeYAML's) class loader doesn't see Beans defined in our plugins. SnakeYAML has anticipated this problem and given us CustomClassLoaderConstructor. To use this:

    Code:
    Yaml y = new Yaml(new CustomClassLoaderConstructor(getClass().getClassLoader()));
    
    or

    Code:
    Yaml y = new Yaml(new CustomClassLoaderConstructor(MyPlugin.class.getClassLoader()));
    where MyPlugin is your JavaPlugin subclass.
     
    md_5 likes this.
  7. Offline

    frelling

    Hafnium Don't worry. I guess I should apologize since I found the CustomClassLoaderConstructor a while back when I revisited SnakeYAML's 1.8 release (helps when you're reading the right JavaDoc version:) ). I should have updated this post and saved you the three hours. My bad.

    Best,

    Frelling
     
  8. Offline

    md_5

    Many diamonds for you!
     
Thread Status:
Not open for further replies.

Share This Page