Objectives

Prepare an suitable version of Eclipse for the forthcoming labs. Download and become familiar with the pacemaker-console project. Explore the Strategy pattern in this context.

Tools

Download and install the following:

Xtend:

Download the latest Eclipse for DSL Developers

Play Framework:

From this site:

in particular this download page:

... and Download and install the latest non-activator version of the Play framework (currently 2.2.6)

Android

From this site:

Download an install the latest Android Studio 2 build.

Pacemaker

Download and import this project into your eclipse workspace:

This is a java version of the pacemaker project you may be familiar with.

The valid commands are documented here:

In addition the above commands, entering '!la' will list all of the commands:

!la

abbrev    name    params
!el    !enable-logging    (fileName)
!dl    !disable-logging    ()
!rs    !run-script    (filename)
!gle    !get-last-exception    ()
!sdt    !set-display-time    (do-display-time)
?l    ?list    (startsWith)
?l    ?list    ()
?h    ?help    (command-name)
?h    ?help    ()
?la    ?list-all    ()
?ghh    ?generate-HTML-help    (file-name, include-prefixed)
l    load    ()
s    store    ()
lu    list-users    ()
cu    create-user    (first name, last name, email, password)
lu    list-user    (email)
lius    list-user    (id)
la    list-activities    (user id)
du    delete-user    (id)
aa    add-activity    (user-id, type, location, distance, datetime, duration)
al    add-location    (activity-id, latitude, longitude)
pm>

and the !rs command will run a specific script. So, if we have a script like this is test.script:

cu homer simpsom homer@simpson.com secret
cu marge simpson marge@simpson.com secret
aa 1 walk  fridge .001  "11:12:2013 11:20:00" 01:20:12
aa 1 walk  bar     1.0  "13:12:2013 02:20:00" 00:00:00
aa 1 run   work    2.2  "14:12:2013 03:20:00" 01:10:00
aa 2 walk  shop    2.5  "15:12:2013 10:20:00" 02:03:00
aa 2 cycle shop    4.5  "16:12:2013 08:20:00" 03:03:00
al 3 23.3 32.3
al 3 23.3 32.5
al 3 23.3 32.6

and we execute:

!rs test.script

then the above users/activities/locations will be added.

If you then enter:

save

.. and refresh the eclipse workspace, it should reveal 'datastore.xml' containing the above entries.

If reatart the program and enter

load

it should restore the entries. Experiment with this now until you are happy it functions as expected.

XMLSerializer

The serialization has been centralised into this class:

public class XMLSerializer
{
  private Deque<Object> stack = new ArrayDeque<>();
  private File file;

  public XMLSerializer(String filename)
  {
    this.file = new File(filename + ".xml");
  }

  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));
      stack = ( Deque<Object>) 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));
      os.writeObject(stack);
    }
    finally
    {
      if (os != null)
      {
        os.close();
      }
    }
  }
}

This is engaged by the PacemakerAPI load/store methods:

  @SuppressWarnings("unchecked")
  public void load() throws Exception
  {
    serializer.read();
    activityIndex = (Long) serializer.pop();
    userIndex     = (Long) serializer.pop();
    activityMap   = (Map<Long, Activity>) serializer.pop();
    userEmailMap  = (Map<String, User>) serializer.pop();
    userMap       = (Map<Long, User>) serializer.pop();
    users         = userMap.values();
  }

  public void store() throws Exception
  {
    serializer.push(userMap);
    serializer.push(userEmailMap);
    serializer.push(activityMap);
    serializer.push(userIndex);
    serializer.push(activityIndex);
    serializer.write();
  }

Which are in turn invoked from PacemakerShell:

  @Command(description="Load activities persistent store")
  public void load () throws Exception
  {
    paceApi.load();
  }

  @Command(description="Store activities persistent store")
  public void store () throws Exception
  {
    paceApi.store();
  }

Make sure you understand the above mechanisms. Perhaps debug into the running program and inspect the program state as you go.

Change File Format Command

We would like to support a new command - cff - short for change file format. This command is to permit xml or json serialisation format.

To save in xml format:

cff xml
save

to save in json:

cff json
save

to load from either:

cff json
load
cff xml
load

This is the implementation of the command in the shell class:

  @Command(description="Set file format")
  public void changeFileFormat (@Param(name="file format: xml, json") String fileFormat)
  {
    if (fileFormat.equals("xml"))
      paceApi.serializer = xmlSerializer;
    else if (fileFormat.equals("json"))
      paceApi.serializer = jsonSerializer; 
  }

Notice that we now have 2 seralizers - one for XML and one for JSON.

Here is an implementation of a JSON serializer :

package utils;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Deque;
import java.util.ArrayDeque;


public class JSONSerializer
{
  private Deque<Object> stack = new ArrayDeque<>();
  private File file;

  public JSONSerializer (String filename)
  {
    this.file = new File(filename + ".json");
  }

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

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

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

    try
    {
      XStream xstream = new XStream(new JettisonMappedXmlDriver());
      is = xstream.createObjectInputStream(new FileReader(file));
      stack = ( Deque<Object>) is.readObject();
    }
    finally
    {
      if (is != null)
      {
        is.close();
      }
    }
  }

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

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

Incorporate this class + the above command into your project.

It will have errors (see next step)

Strategy

See if you can complete the implementation such that the cff command works as expected.

This will require:

  • a Serializer interface
  • this interface is to be implemented by XMLSerializer & JSONSerializer
  • refactor Pacemaker API to use this interface instead of the concrete classes

The overall effect should be to keep knowledge of the specific serializer away from the PacemanerAPI class.

The solution is presented in the next step.

Exercises

Solution:

This is a solution:

These three commits encapsulate the application of the pattern:

Verify that this is equivalent to your solution.

Exercise:

Consider introducing a new serializer to support the YAML file format:

This is a prominent java YAML library:

Here is a version of a write method that will serialize our stack data structure:

  public void write() throws Exception
  {
    Yaml yaml = new Yaml();
    FileWriter writer = new FileWriter(file);
    for (Object o : stack)
    {
      yaml.dump (stack.pop(), writer);
    }
    writer.close();
  }

Try this out and explore the generated output.

Is it possible to seamlessly integrate this into pacemaker as another serializer?