Section 2

Dec 24, 2016
Section 2
  • YAML

    In the previous section, we covered how to serialize a Cat object into a String:
    Code (Text):
    %uuid%#71445d04-3aee-4fe9-91d7-82ac3067aebc#%name%#Tom#%age%#3#$allergies$&%description%#Allergic to eggnog#%duration%#139#,%description%#Allergic to other cats#%duration%#245#&$medicalNotes$&%description%#Fractured bone#%severe%#true#,%description%#Stomach Ache#%severe%#false#&
    It doesn't look very pretty though, does it? That's one of the main issues with writing our own serialization method. Our cat hospital doesn't have time to wait for pretentious developers trying to define their own fancy format, so let's use one that's already been made: YAML.

    Yet Another Markup Language Ain't A Markup Language (YAML) is a format used for serializing information. Despite its atrocious name, it provides a neat method for formatting data in a way that makes it easy to store in plain files. Bukkit natively supports it too. :)

    Before we start, just remember we're going to be using the YAML libraries provided by Spigot. There's several libraries out there you can choose from though, so your options are expansive. Also, if you're looking for a tutorial on how to create, read, and write from YAML files, then you're in the wrong place. This tutorial section only covers how to serialize and deserialize and object using YAML.

    Let's start by using our magical three classes (PatientCat, Allergy, and MedicalNote):

    PatientCat:
    Code (Text):

    public class PatientCat // We'll use this class to keep track of each cat patient's medical information
    {
        // Needed Fields
        private final UUID uuid; // Prevent collisions with other cats (Sometimes cat's will have the same name, odd isn't it?)
        private final String name; // Gives us an easier way of referring to a cat
        private final int age; // How old the cat is (in 'human years')
        private Allergy[] allergies; // An array containing the cat's allergy information.
        private MedicalNote[] medicalNotes; // An array containing the cat's medical information. (Can be broken bones, tumors, infections, etc.)

        //Pretty basic constructor. All we're doing here is creating a cat object with all the parameters we need.
        public PatientCat(UUID uuid, String name, int age, Allergy[] allergies, MedicalNote[] medicalNotes)
        {
            this.uuid = uuid;
            this.name = name;
            this.age = age;
            this.allergies = allergies;
            this.medicalNotes = medicalNotes;
        }

        // Getters (Allows us to get information from a cat object)

        public UUID getUUID()
        {
            return this.uuid;
        }

        public String getName()
        {
            return this.name;
        }

        public int getAge()
        {
            return this.age;
        }

        public Allergy[] getAllergies()
        {
            return this.allergies;
        }

        public MedicalNote[] getMedicalNotes()
        {
            return this.medicalNotes;
        }
    }
     
    Allergy:
    Code (Text):

    public class Allergy // This class is used for storing a cat's allergy information
    {
        // Needed Fields
        private final String description; // Describe the allergy
        private final int duration; // How long the allergy lasts in minutes

        // Basic constructor that accepts the values for the bits of information we need
        public Allergy(String description, int duration)
        {
            this.description = description;
            this.duration = duration;
        }

        // Getters

        public String getDescription()
        {
            return this.description;
        }

        public int getDuration()
        {
            return this.duration;
        }
    }
     
    MedicalNote:
    Code (Text):

    public class MedicalNote
    {
        // Needed Fields
        private String description; // Describe exactly what's wrong (or what could go wrong) with the cat
        private boolean severe; // Whether or not the medical note is severe

        // Constructor that accepts the values we need
        public MedicalNote(String description, boolean severe)
        {
            this.description = description;
            this.severe = severe;
        }

        // Getters

        public String getDescription()
        {
            return this.description;
        }

        public boolean isSevere()
        {
            return this.severe;
        }
    }
     
    Now, if we want to serialize an object using the libraries provided by Spigot, our classes need to implement the ConfigurationSerializable interface. This requires three things we need to do:

    1. Override the serialize method
    2. Create a constructor that accepts a serialized map
    3. Register the class

    Let's start with overriding the serialize method. This method has no parameters, but requires that we return a Map with a String key and Object value. This is how we'll serialize the class' information. By putting each field and its respective value into the map and returning it to Bukkit:

    PatientCat class:
    Code (Text):

    public class PatientCat // We'll use this class to keep track of each cat patient's medical information
    {
        @Override // Override the serialize method from ConfigurationSerializeable
        public Map<String, Object> serialize()
        {
            HashMap<String, Object> mapSerializer = new HashMap<>(); // Create a map that will be used to serialize the class's fields

            // Add the field and its value (the key and value):
            mapSerializer.put("uuid", this.uuid.toString()); // This is a 'special' type of object, so it will be 'converted' into a string
            mapSerializer.put("name", this.name);
            mapSerializer.put("age", this.age);

            ArrayList<Map<String, Object>> tempSerializeAllergies = new ArrayList<>(); // Using this as temporary container allowing us to serialize each allergy into a list
            for(Allergy allergy : this.allergies)
                tempSerializeAllergies.add(allergy.serialize()); // Serialize this allergy and add it into the temporary list

            ArrayList<Map<String, Object>> tempSerializeMedicalNotes = new ArrayList<>(); // Same thing as the allergies, except this time we're doing Medical Notes
            for(MedicalNote medicalNote : this.medicalNotes)
                tempSerializeMedicalNotes.add(medicalNote.serialize()); // Serialize this medical note and add it into the temporary list

            mapSerializer.put("allergies", tempSerializeAllergies); // Put the serialized allergies into the map
            mapSerializer.put("medicalNotes", tempSerializeMedicalNotes); // Put the serialized medical notes into the map

            return mapSerializer; // Return the map containing all the cat patient's information
        }

       // Getters are below here
    }
     
    Okay now let's do the same thing for the Allergy and MedicalNote classes:

    Allergy:
    Code (Text):

    public class Allergy
    {
      // Constructor above here

      @Override // Override the serialize method from ConfigurationSerializeable
      public Map<String, Object> serialize()
      {
      HashMap<String, Object> mapSerializer = new HashMap<>(); // Create a map that will be used to serialize the class's fields

      // Add the field and its value (the key and value):
      mapSerializer.put("description", this.description);
      mapSerializer.put("duration", this.duration);

      return mapSerializer; // Return the map containing all the cat patient's information
      }

      // Getters below here
    }
    [/SIZE]
     
    MedicalNote:
    Code (Text):

    public class MedicalNote
    {
      // Constructor above here

      @Override // Override the serialize method from ConfigurationSerializeable
      public Map<String, Object> serialize()
      {
      HashMap<String, Object> mapSerializer = new HashMap<>(); // Create a map that will be used to serialize the class's fields

      // Add the field and its value (the key and value):
      mapSerializer.put("description", this.description);
      mapSerializer.put("severe", this.severe);

      return mapSerializer; // Return the map containing all the cat patient's information
      }

      // Getters below here
    }
    [/SIZE]
     
    Alright, now that we have the serialization methods we can work on deserialization. Before that though, I want to discuss a few things with how objects are serialized. For the PatientCat class, we serialized each allergy/medical-note individually, added it to a temporary array, then put it into the map. Why? Due to the design of the libraries we're using, we need to call the serialized method for nested child classes. They need to be turned into a map object, because Bukkit isn't the one who puts everything into the map; we are. Because YAML is structured like a tree, everything should be mapped out and that's what we're doing here. It might seem weird, but it's just how the libraries have been designed. As long as you remember to do this, serialization with YAML should be easy. ;)

    Because we have the ability to serialize everything ourselves, we can define what gets serialized and what doesn't. Say for example, in the PatientCat class. Maybe we don't care about the cat's name, only the UUID. If we don't need its name, then we don't need to put it into the map; thus:
    Code (Text):
    mapSerializer.put("name", this.name);
    can be removed entirely. So if you have an object you don't want to be serialized (or an object that doesn't support being serialized) then simply don't put it into the map. :)

    Let's work on deserialization now. In this case, we'll be using a constructor to build an object from a map (just like the ones we used to put all our fields and values into). It's really simple to do this:

    PatientCat:
    Code (Text):

    public class PatientCat
    {
      // Other Constructor above here

      public PatientCat(Map<String, Object> serializedPatientCat)
      {
      // All we need to do here is read from the map using the keys we defined in our serialize method (Because the values are objects, ensure that you cast them)
      this.uuid = UUID.fromString((String) serializedPatientCat.get("uuid")); // This is a 'special' case since we need to 'convert' the uuid from a string to an UUID object
      this.name = (String) serializedPatientCat.get("name");
      this.age = (int) serializedPatientCat.get("age");

      // Alright, let's deserialize the allergies and medical-notes. We're expecting the objects to be a list of maps, so we'll treat them as such
      ArrayList<Map<String, Object>> mappedAllergies = (ArrayList<Map<String, Object>>) serializedPatientCat.get("allergies");
      ArrayList<Allergy> deserializedAllergyContainer = new ArrayList<>(); // temporary container for deserialized allergies
      for(Map<String, Object> serializedAllergy : mappedAllergies)
      deserializedAllergyContainer.add(new Allergy(serializedAllergy)); // All we need to do is give the 'special' deserialization constructor the serialized allergy map, then add it into the temporary array list

      // Same thing for the medical notes
      ArrayList<Map<String, Object>> mappedMedicalNotes = (ArrayList<Map<String, Object>>) serializedPatientCat.get("medicalNotes");
      ArrayList<MedicalNote> deserializedMedicalNoteContainer = new ArrayList<>(); // temporary container for deserialized medical notes
      for(Map<String, Object> serializedMedicalNote : mappedMedicalNotes)
      deserializedMedicalNoteContainer.add(new MedicalNote(serializedMedicalNote)); // same thing as we did for the allergy

      // Give the arrays their respective references
      this.allergies = (Allergy[]) deserializedAllergyContainer.toArray();
      this.medicalNotes = (MedicalNote[]) deserializedMedicalNoteContainer.toArray();
      }

      // Getters below here
    }
     
    Allergy:
    Code (Text):

    public class Allergy
    {
      // Other constructor above here

      public Allergy(Map<String, Object> serializedAllergy)
      {
      // All we need to do here is read from the map using the keys we defined in our serialize method (Because the values are objects, ensure that you cast them)
      this.description = (String) serializedAllergy.get("description");
      this.duration = (int) serializedAllergy.get("duration");
      }

      // Getters below here
    }
     
    MedicalNote:
    Code (Text):

    public class MedicalNote
    {
      // Other constructor above here

      public MedicalNote(Map<String, Object> serializedMedicalNote)
      {
      // All we need to do here is read from the map using the keys we defined in our serialize method (Because the values are objects, ensure that you cast them)
      this.description = (String) serializedMedicalNote.get("description");
      this.severe = (boolean) serializedMedicalNote.get("severe");
      }

      // Getters below here
    }
     
    All we need to do is register them now. I recommend registering them in the onEnable method within your plugin:
    Code (Text):

    @Override // Register our classes with Bukkit
    public void onEnable()
    {
        ConfigurationSerialization.registerClass(PatientCat.class)
        ConfigurationSerialization.registerClass(Allergy.class);
        ConfigurationSerialization.registerClass(MedicalNote.class);
    }
     
    Now we can serialize and deserialize our objects:
    Code (Text):

    FileConfiguration myConfig; // This reference would be obtained how you normally get it, if you don't understand how to do this, check the tutorial link I posted above

    Allergy[] allergies = new Allergy[2]; // Create an array that hold's this cat's allergies
    allergies[0] = new Allergy("Allergic to eggnog", 139); // reference index 0 to this allergy
    allergies[1] = new Allergy("Allergic to other cats", 245); // reference index 1 to this allergy

    MedicalNote[] medicalNotes = new MedicalNote[2]; // Create an array that hold's this cat's medical notes
    medicalNotes[0] = new MedicalNote("Fracture bone", true); // reference index 0 to this medical note
    medicalNotes[1] = new MedicalNote("Stomach Ache", false); // reference index 1 to this medical note

    UUID randomUUID = UUID.randomUUID(); // Create a random UUID (just for tutorial purposes)

    PatientCat patientCat = new PatientCat(randomUUID, "Tom", 3, allergies, medicalNotes); // Create the cat (uuid, name, age, allergies, and medical notes)

    myConfig.set("hospital.patients." + patientCat .getUUID(), patientCat ); // Serialize the cat and write it to the value at the specified path

    PatientCat deserializedPatientCat = (PatientCat) myConfig.get("hospital.patients." + patientCat.getUUID());

    System.out.println("Yay, we found: " + patientCat.getName() + " who seems to be " + patientCat.getAllergies()[0] + "!"); // Print a message letting us know it worked correctly
     
    The result would look something like this:
    Code (Text):

    123e4567-e89b-12d3-a456-426655440000:
      age: 3
      allergies:
      -
      description: "Allergic to eggnog"
      duration: 139
      -
      description: "Allergic to other cats"
      duration: 345
      medicalNotes:
      -
      description: "Fracture bone"
      severe: true
      -
      description: "Stomach Ache"
      severe: false
      name: Tom
      uuid: 123e4567-e89b-12d3-a456-426655440000
     

    Easier than writing our own method, no? While it is a bit of work to setup, it can be more rewarding than spending a lot of time creating a customized format. It's much easier to visualize and edit, includes support from various vendors, and has been much more standardized. There's lot's to love with the YAML format.

    When should I use this method?
    This method is best if you intend to create a plugin downloaded by server owners. It provides them an easy way of configuring and accessing stored/serialized data. Also, it's easier to setup, so if you need a quick way of serializing data then I'd also recommend its use.

    Pros of using this method:

    • Supported by different vendors
    • Easy to setup, read, and modify through files
    • Great for creating plugins for basic users

    Cons of using this method:
    • Difficult to implement more complex data structures
    • Not designed for SQL database storage
  • Loading...
  • Loading...