Serializing ItemMeta and All Your Wildest Dreams

Discussion in 'Resources' started by evilmidget38, Mar 26, 2013.

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

    evilmidget38

    /*
    Show Spoiler
    So, as I've been around a while, everyone, and their dog even, seems to have trouble with serializing ItemMeta. I've seen all sorts of crazy solutions for doing this, some of which good, some of which.... Not so much. So, I have come forth to set things straight, and explain the great process of serializing ItemStacks with their ItemMeta using ONLY the Bukkit API. That's right, no OBC, no NMS, just pure Bukkit and all its beauty.

    This is specifically for saving to formats other than YAML. If you're using YAML, there's easy API support for it.

    Complete class file:
    https://gist.github.com/evilmidget38/5251102

    I know, I didn't comment the code. Read this tutorial if you want to understand how it works. Maybe I'll go through and comment it later, but for now, that's what you get.

    Now, on to the actual code. I like to explain things in a problem-solving manner, explaining logically how you go about solving this problem, followed by the step by step process of actually doing it.

    So, lets start by making our method declaration. I'm thinking something nice and short, like
    Code:java
    1. public static List<Map<String, Object>> serializeConfigurationSerializableList(List<ConfigurationSerializable> list)

    Okay, maybe that's a little long...

    Regardless, we want to turn our List of ConfigurationSerializable objects(ItemStacks, in our case) into their serialized counterpart (Map<String, Object>). Sounds easy, right? Let's just use ItemStack#serialize() and return that! (yes, I know I changed the name of the method)
    Code:java
    1.  
    2. public static List<Map<String, Object>> serializeItemList(List<ConfigurationSerializable> list) {
    3. List<Map<String, Object>> returnVal = new ArrayList<Map<String, Object>>();
    4. for (ConfigurationSerializable cs : list) {
    5. returnVal.add(cs.serialize());
    6. }
    7. return returnVal;
    8. }
    9.  

    NOPENOPENOPE

    That won't work for a number of reasons. First and formost, it won't have the serialized type inside of the map, which means although you can serialize it, you won't be able to deserialize it. Additionally, if it's an ItemStack, your ItemMeta won't be deserialized. These two flaws make using ItemStack#serialize() by itself useless. That's why we need to use some sorcery and black magic to accomplish our task.

    So, the first issue we have is that we don't have the serialized type within our map.

    I know, let's add that to our map!
    Code:java
    1.  
    2. public static List<Map<String, Object>> serializeItemList(List<ConfigurationSerializable> list) {
    3. List<Map<String, Object>> returnVal = new ArrayList<Map<String, Object>>();
    4. for (ConfigurationSerializable cs : list) {
    5. Map<String, Object> serialized = cs.serialize();
    6. serialized.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(cs.getClass())); ***
    7. returnVal.add(serialized);
    8. }
    9. return returnVal;
    10. }
    11.  

    I am using ConfiguratinoSerialization.SERIALIZED_TYPE_KEY and ConfigurationSerialization.getAlias(Class<?>) for a reason! Using SERIALIZED TYPE KEY means you don't have to rely on magic Strings(even if they're unlikely to change, relying on magic Strings is always bad). Using ConfigurationSerialization.getAlias(Class<?>) is used so that you use the correct alias when serializing or deserializing the ConfigurationSerializable. If we simply used the class of the object, we might end up using org.bukkit.craftbukkit.inventory.CraftItemStack instead of ItemStack. This prevents us from deserializing it correctly later. If you don't use getAlias(Class<?>), this will not work!

    Now, we're looking good, right? Nope! Bukkit likes ImmutableMaps, so this isn't going to work so well for us. This works fine for saving to Bukkit's Configuration, as the data is copied from the ImmutableMap to the Map backing the Configuration. So, let's make our own map. The best way to do this, IMO, is to recursively go through the ImmutableMap and copy it to our own map. There's no need to copy replace any of the maps inside of it though, as we're doing this only so we can modify the map to add the serialized type. We'll use HashMaps, but most standard Map implementations will work fine.

    Code:java
    1.  
    2. public static List<Map<String, Object>> serializeItemList(List<ConfigurationSerializable> list) {
    3. List<Map<String, Object>> returnVal = new ArrayList<Map<String, Object>>();
    4. for (ConfigurationSerializable cs : list) {
    5. Map<String, Object> serialized = recreateMap(cs.serialize());
    6. serialized.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(cs.getClass()));
    7. returnVal.add(serialized);
    8. }
    9. return returnVal;
    10. }
    11.  
    12. public static Map<String, Object> recreateMap(Map<String, Object> original) {
    13. Map<String, Object> map = new HashMap<String, Object>();
    14. for (Entry<String, Object> entry : original.entrySet()) {
    15. map.put(entry.getKey(), entry.getValue());
    16. }
    17. return map;
    18. }
    19.  


    Alright, that's great for Serializing an ItemStack and all, but we still haven't handled the ItemMeta or any other ConfigurationSerializable object that might be within the Map. So, let's do that. Let's go through the Map, and serialize anything that's ConfigurationSerializable using the same thing we've done before. The easiest way to do this, IMO, is to simply extract the serialization of the ConfigurationSerializable object to its own method.
    Code:java
    1.  
    2. public static List<Map<String, Object>> serializeItemList(List<ConfigurationSerializable> list) {
    3. List<Map<String, Object>> returnVal = new ArrayList<Map<String, Object>>();
    4. for (ConfigurationSerializable cs : list) {
    5. returnVal.add(serialize(cs));
    6. }
    7. return returnVal;
    8. }
    9.  
    10. public static Map<String, Object> serialize(ConfigurationSerializable cs) {
    11. Map<String, Object> serialized = recreateMap(cs.serialize());
    12. for (Entry<String, Object> entry : serialized.entrySet()) {
    13. if (entry.getValue() instanceof ConfigurationSerializable) {
    14. entry.setValue(serialize((ConfigurationSerializable)entry.getValue()));
    15. }
    16. }
    17. serialized.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(cs.getClass()));
    18. return serialized;
    19. }
    20.  
    21. public static Map<String, Object> recreateMap(Map<String, Object> original) {
    22. Map<String, Object> map = new HashMap<String, Object>();
    23. for (Entry<String, Object> entry : original.entrySet()) {
    24. map.put(entry.getKey(), entry.getValue());
    25. }
    26. return map;
    27. }
    28.  

    So, that's what we've got so far. We can easily serialize any ConfigurationSerializable object, and even a list of them. That's handy and all, but how do we get them back? This is by far the easiest part of the entire process. The delicate handling of serialization ensures this.
    Code:java
    1.  
    2. // Time for Deserialization
    3. @SuppressWarnings("unchecked")
    4. public static ConfigurationSerializable deserialize(Map<String, Object> map) {
    5. for (Entry<String, Object> entry : map.entrySet()) {
    6. // Check if any of its sub-maps are ConfigurationSerializable. They need to be done first.
    7. if (entry.getValue() instanceof Map && ((Map)entry.getValue()).containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
    8. entry.setValue(deserialize((Map)entry.getValue()));
    9. }
    10. }
    11. return ConfigurationSerialization.deserializeObject(map);
    12. }
    13.  
    14. public static List<ConfigurationSerializable> deserializeItemList(List<Map<String, Object>> itemList) {
    15. List<ConfigurationSerializable> returnVal = new ArrayList<ConfigurationSerializable>();
    16. for (Map<String, Object> map : itemList) {
    17. returnVal.add(deserialize(map));
    18. }
    19. return returnVal;
    20. }
    21.  

    As you can see, deserialization is super easy. A few basic method calls, and we're done.

    This method of serialization and deserialization works for any and all ConfigurationSerializable objects, and is fairly future proof. Perhaps to improve the future proof-ness you could check for Collections of ConfigurationSerializable objects, but I'll leave that up to you.

    All that's left now is saving it to whatever you want. Want to store this Map in SQL using an OOS? Go ahead. Want to save it to JSON? Go ahead. As long as you can retrieve the map later, you'll be able to deserialize it just fine using this method.

    */

    Thanks to work by Wolvereness, all of the previous work is unnecessary. You can now use a BukkitObjectOutputStream exactly like you would have used an ObjectOutputStream, and you can use a BukkitObjectInputStream exactly like you would have used an ObjectInputStream.
     
    Maulss, Goblom, microgeek and 11 others like this.
  2. Offline

    InflamedSebi

  3. Offline

    microgeek

    It looks like Evil's method allows support for storing(and deserializing) the serialized data somewhere other else than yaml.
     
  4. Offline

    evilmidget38

    That method writes the data into a yaml file, and then takes the contents of the yaml file as a String and saves them. My method instead simply converts it into an OOS friendly map suitable for saving in whatever format you want.
     
    1 person likes this.
  5. when you recreating your collection objects like maps and lists, pass the expected size into the constructor, will decrease some time if the map/list is large.
     
  6. Offline

    SquidDevelops

    evilmidget38 So is it that we would store it to mySQL as a blob and how would we go about storing it as a blob with husky's SQL methods? Would we store playername and blob in separate colems or just give it the blob and how would we give it as the blob. Just give it the map in my update ? Sorry for asking so many questions I've been trying to get a ItemStack[] or Inventory stored in mySQL for atleast 2 days of non stop working to attempt to get a working serialization that I can properly store in mySQL, I have tried NMS, protocollib, and much more :/
     
  7. Offline

    evilmidget38

    Thread updated to reflect a new feature addition in Bukkit.
     
    1 person likes this.
  8. Offline

    xWatermelon

    evilmidget38 can you update the OP to give an example on usage with BukkitObjectOutput/InputStreams?
     
  9. Offline

    evilmidget38

    xWatermelon

    Code:java
    1.  
    2. ItemStack item = new ItemStack(Material.DIAMOND, 1);
    3. List<ItemStack> items = new ArrayList<ItemStack>();
    4. items.add(item);
    5. items.add(item);
    6. items.add(item);
    7. try {
    8. BukkitObjectOutputStream boos = new BukkitObjectOutputStream(baos);
    9. boos.writeObject(items);
    10. boos.close();
    11. } catch (IOException ioexception) {
    12. ioexception.printStackTrace();
    13. }
    14. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    15. Object backFromTheDead = null;
    16. try {
    17. BukkitObjectInputStream bois = new BukkitObjectInputStream(bais);
    18. backFromTheDead = bois.readObject();
    19. bois.close();
    20. } catch (IOException ioexception) {
    21. ioexception.printStackTrace();
    22. } catch (ClassNotFoundException classNotFoundException) {
    23. classNotFoundException.printStackTrace();
    24. }
    25. List<ItemStack> returnedItemList = (List<ItemStack>) backFromTheDead;
    26.  


    You can easily replace ByteArrayOuptutStream with FileOutputStream or really any other OutputStream. Just make sure you have consistent input and output streams.
     
    lukegb likes this.
  10. Offline

    alexschrod

    Thank you so much for this. I will obviously switch to BukkitObjectOutputStream/InputStream once the server I'm coding for makes the switch, but this will hold us over just fine in the meantime. :)

    I had one "issue," which was that if you try to pass the ItemStack[] you get from PlayerInventory.getContents() directly in to your serializeItemList() method through Arrays.asList(), you'll get a NullPointerException in your code. I just thought that was weird, but I'm guessing your usage never involved doing that specifically, since it's not handled.

    In any case, I worked around this by looping over the ItemStack[] array and replace any nulls with a "new ItemStack(Material.AIR)" before sending them into the serializer. Probably not the most efficient fix, but like I said at the beginning, this is all just to hold us over until BukkitObjectOutputStream/InputStream is available to us.
     
Thread Status:
Not open for further replies.

Share This Page