Objectives

Extend the pacemaker application to incorporate a serialization mechanism to enable users & activities to be persisted to a file. We will then try to generalize this mechanism, which will enable us to experiment with alternative serialization formats.

Serialization

We would like to incorporate persistence into our API. First, to prove that we can get it off the ground correctly, we can try some small experiments in main. This should be your current main method:

    PacemakerAPI pacemakerAPI = new PacemakerAPI();

    pacemakerAPI.createUser("Bart", "Simpson",   "bart@simpson.com", "secret");
    pacemakerAPI.createUser("Homer", "Simpson",  "homer@simpson.com", "secret");
    pacemakerAPI.createUser("Lisa", "Simpson", " lisa@simpson.com", "secret");

    Collection<User> users = pacemakerAPI.getUsers();
    System.out.println(users);

    User homer = pacemakerAPI.getUserByEmail("homer@simpson.com");
    System.out.println(homer);

    pacemakerAPI.deleteUser(homer.id);
    users = pacemakerAPI.getUsers();
    System.out.println(users);

Extend the above with the following:

    XStream xstream = new XStream(new DomDriver());
    ObjectOutputStream out = xstream.createObjectOutputStream(new FileWriter("datastore.xml"));
    out.writeObject(users);
    out.close();

And run the main program. A file called datastore.xml is generated:

<object-stream>
  <java.util.HashMap_-Values>
    <outer-class>
      <entry>
        <long>0</long>
        <models.User>
          <id>0</id>
          <firstName>Bart</firstName>
          <lastName>Simpson</lastName>
          <email>bart@simpson.com</email>
          <password>secret</password>
          <activities/>
        </models.User>
      </entry>
      <entry>
        <long>2</long>
        <models.User>
          <id>2</id>
          <firstName>Lisa</firstName>
          <lastName>Simpson</lastName>
          <email> lisa@simpson.com</email>
          <password>secret</password>
          <activities/>
        </models.User>
      </entry>
    </outer-class>
  </java.util.HashMap_-Values>
</object-stream>

Look at this file carefully. Add some activities and locations pacemaker object, and explore the contents of the xml file when you save it again.

Pacemaker API

To introduce persistence capability into the api, we need two new methods in PacemakerApi:

Here they are:

  @SuppressWarnings("unchecked")
  void load(File file) throws Exception
  {
    ObjectInputStream is = null;
    try
    {
      XStream xstream = new XStream(new DomDriver());
      is = xstream.createObjectInputStream(new FileReader(file));
      userIndex       = (Map<Long, User>)     is.readObject();
      emailIndex      = (Map<String, User>)   is.readObject();
      activitiesIndex = (Map<Long, Activity>) is.readObject();
    }
    finally
    {
      if (is != null)
      {
        is.close();
      }
    }
  }

  void store(File file) throws Exception
  {
    XStream xstream = new XStream(new DomDriver());
    ObjectOutputStream out = xstream.createObjectOutputStream(new FileWriter(file));
    out.writeObject(userIndex);
    out.writeObject(emailIndex);
    out.writeObject(activitiesIndex);
    out.close(); 
  }

Take some time to read these now - and try them out store from the main method, replacing the fragment we inserted in the last step:

    pacemakerAPI.store(new File("datastore.xml"));

Can you figure out how to load a pacemaker file?

Generalizing the Serializer

We have implemnted an xstream based serialization or our model. However, we may be interested in revising this later, selecting a different format or even serialiation component. Lets try to abstract the serializer feature a little so we can prepare for such a switch.

First, introduce into the utils package a new interface:

package utils;

public interface Serializer
{
  void push(Object o);
  Object pop();
  void write() throws Exception;
  void read() throws Exception;
}

... and then an implementation of this interface, using the xtream library we have been using:

package utils;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Stack;

public class XMLSerializer implements Serializer
{

  private Stack stack = new Stack();
  private File file;

  public XMLSerializer(File file)
  {
    this.file = file;
  }

  public void push(Object o)
  {
    stack.push(o);
  }

  public Object pop()
  {
    return stack.pop(); 
  }

  @SuppressWarnings("unchecked")
  public void read() throws Exception
  {
    ObjectInputStream is = null;

    try
    {
      XStream xstream = new XStream(new DomDriver());
      is = xstream.createObjectInputStream(new FileReader(file));
      Object obj = is.readObject();
      while (obj != null)
      {
        stack.push(obj);
        obj = is.readObject();
      }
    }
    finally
    {
      if (is != null)
      {
        is.close();
      }
    }
  }

  public void write() throws Exception
  {
    ObjectOutputStream os = null;

    try
    {
      XStream xstream = new XStream(new DomDriver());
      os = xstream.createObjectOutputStream(new FileWriter(file));
      while (!stack.empty())
      {
        os.writeObject(stack.pop());  
      }
    }
    finally
    {
      if (os != null)
      {
        os.close();
      }
    }
  }
}

PacemakerAPI Updates

Our PacemakerAPI class can now make use of the serializer instead of getting involved in the work of serialization itself:

public class PacemakerAPI
{
  //...

  private Serializer serializer;

  public PacemakerAPI()
  {
  }

  public PacemakerAPI(Serializer serializer)
  {
    this.serializer = serializer;
  }

  @SuppressWarnings("unchecked")
  public void load() throws Exception
  {
    serializer.read();
    activitiesIndex = (Map<Long, Activity>) serializer.pop();
    emailIndex      = (Map<String, User>)   serializer.pop();
    userIndex       = (Map<Long, User>)     serializer.pop();
  }

  void store() throws Exception
  {
    serializer.push(userIndex);
    serializer.push(emailIndex);
    serializer.push(activitiesIndex);
    serializer.write(); 
  }
  //...
}

Now rework the Main class to attempt to load from the datastore in a constructor for Main:

    File  datastore = new File("datastore2.xml");
    Serializer serializer = new XMLSerializer(datastore);

    PacemakerAPI pacemakerAPI = new PacemakerAPI(serializer);
    if (datastore.isFile())
    {
      pacemakerAPI.load();
    }

    pacemakerAPI.createUser("Bart", "Simpson",   "bart@simpson.com", "secret");
    pacemakerAPI.createUser("Homer", "Simpson",  "homer@simpson.com", "secret");
    pacemakerAPI.createUser("Lisa", "Simpson", " lisa@simpson.com", "secret");

    Collection<User> users = pacemakerAPI.getUsers();
    System.out.println(users);

    User homer = pacemakerAPI.getUserByEmail("homer@simpson.com");
    pacemakerAPI.createActivity(homer.id, "walk", "tramore", 1000);

    pacemakerAPI.store();

Open datastore2.xml and have a close look:

<object-stream>
  <map>
    <entry>
      <long>0</long>
      <models.Activity>
        <id>0</id>
        <type>walk</type>
        <location>tramore</location>
        <distance>1000.0</distance>
        <route/>
      </models.Activity>
    </entry>
  </map>
  <map>
    <entry>
      <string>homer@simpson.com</string>
      <models.User>
        <id>1</id>
        <firstName>Homer</firstName>
        <lastName>Simpson</lastName>
        <email>homer@simpson.com</email>
        <password>secret</password>
        <activities>
          <entry>
            <long>0</long>
            <models.Activity>
              <id>0</id>
              <type>walk</type>
              <location>tramore</location>
              <distance>1000.0</distance>
              <route/>
            </models.Activity>
          </entry>
        </activities>
      </models.User>
    </entry>
    <entry>
      <string> lisa@simpson.com</string>
      <models.User>
        <id>2</id>
        <firstName>Lisa</firstName>
        <lastName>Simpson</lastName>
        <email> lisa@simpson.com</email>
        <password>secret</password>
        <activities/>
      </models.User>
    </entry>
    <entry>
      <string>bart@simpson.com</string>
      <models.User>
        <id>0</id>
        <firstName>Bart</firstName>
        <lastName>Simpson</lastName>
        <email>bart@simpson.com</email>
        <password>secret</password>
        <activities/>
      </models.User>
    </entry>
  </map>
  <map>
    <entry>
      <long>0</long>
      <models.User>
        <id>0</id>
        <firstName>Bart</firstName>
        <lastName>Simpson</lastName>
        <email>bart@simpson.com</email>
        <password>secret</password>
        <activities/>
      </models.User>
    </entry>
    <entry>
      <long>1</long>
      <models.User>
        <id>1</id>
        <firstName>Homer</firstName>
        <lastName>Simpson</lastName>
        <email>homer@simpson.com</email>
        <password>secret</password>
        <activities>
          <entry>
            <long>0</long>
            <models.Activity>
              <id>0</id>
              <type>walk</type>
              <location>tramore</location>
              <distance>1000.0</distance>
              <route/>
            </models.Activity>
          </entry>
        </activities>
      </models.User>
    </entry>
    <entry>
      <long>2</long>
      <models.User>
        <id>2</id>
        <firstName>Lisa</firstName>
        <lastName>Simpson</lastName>
        <email> lisa@simpson.com</email>
        <password>secret</password>
        <activities/>
      </models.User>
    </entry>
  </map>
</object-stream>

What is it that is wrong with the above? (Hint now many 'barts' should there be?)

Object References and Serialization

The problem with our current serializer is that the three Maps serialized are completely independent - even though the maps in memory prior to serialization contain shared objects. This is apparent from this structural view here:

Correcting this is relatively straightforward. Object referential context is only preserved over a single writeObject operation - we have been doing several on our serializer - see the relevant fragments of the read and write methods:

write

      while (!stack.empty())
      {
        os.writeObject(stack.pop());  
      }

read

      Object obj = is.readObject();
      while (obj != null)
      {
        stack.push(obj);
        obj = is.readObject();
      }

Replace the above fragments with single read/write operation:

write

      os.writeObject(stack);

read

      stack = (Stack) is.readObject();

To test this, change main to use a different file:

    File  datastore = new File("datastore3.xml");

When you run main, it produces a different serialization structure - with objects shared via references. You can see this more easily from the structural view:

Also explore the xml aspects - can you see the difference form datastore2.xml - which should still be your project?

Exercises

Archive of project so far...

Tests

Start to think about how you can test the new persistence facility we have just introduced.