Extend the pacemaker application to include Activity and Location classes + associated commands. Once these are in place, 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.
These are solution classes to last weeks exercises:
package models;
import static com.google.common.base.MoreObjects.toStringHelper;
import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Objects;
public class Activity
{
static Long counter = 0l;
public Long id;
public String type;
public String location;
public double distance;
public List<Location> route = new ArrayList<>();
public Activity()
{
}
public Activity(String type, String location, double distance)
{
this.id = counter++;
this.type = type;
this.location = location;
this.distance = distance;
}
@Override
public String toString()
{
return toStringHelper(this).addValue(id)
.addValue(type)
.addValue(location)
.addValue(distance)
.addValue(route)
.toString();
}
@Override
public int hashCode()
{
return Objects.hashCode(this.id, this.type, this.location, this.distance);
}
}
package models;
import static com.google.common.base.MoreObjects.toStringHelper;
import com.google.common.base.Objects;
public class Location
{
static Long counter = 0l;
public Long id;
public float latitude;
public float longitude;
public Location()
{
}
public Location (float latitude, float longitude)
{
this.id = counter++;
this.latitude = latitude;
this.longitude = longitude;
}
@Override
public String toString()
{
return toStringHelper(this).addValue(id)
.addValue(latitude)
.addValue(longitude)
.toString();
}
@Override
public int hashCode()
{
return Objects.hashCode(this.id, this.latitude, this.longitude);
}
}
These are the new features for PacemakerAPI
public void createActivity(Long id, String type, String location, double distance)
{
Activity activity = new Activity (type, location, distance);
Optional<User> user = Optional.fromNullable(userIndex.get(id));
if (user.isPresent())
{
user.get().activities.put(activity.id, activity);
activitiesIndex.put(activity.id, activity);
}
}
public Activity getActivity (Long id)
{
return activitiesIndex.get(id);
}
public void addLocation (Long id, float latitude, float longitude)
{
Optional<Activity> activity = Optional.fromNullable(activitiesIndex.get(id));
if (activity.isPresent())
{
activity.get().route.add(new Location(latitude, longitude));
}
}
...and these are the new command implementations:
@Command(description="Add an activity")
public void addActivity (@Param(name="user-id") Long id, @Param(name="type") String type,
@Param(name="location") String location, @Param(name="distance") double distance)
{
Optional<User> user = Optional.fromNullable(paceApi.getUser(id));
if (user.isPresent())
{
paceApi.createActivity(id, type, location, distance);
}
}
@Command(description="Add Location to an activity")
public void addLocation (@Param(name="activity-id") Long id,
@Param(name="latitude") float latitude, @Param(name="longitude") float longitude)
{
Optional<Activity> activity = Optional.fromNullable(paceApi.getActivity(id));
if (activity.isPresent())
{
paceApi.addLocation(activity.get().id, latitude, longitude);
}
}
The above code uses a feature of the Guava library that we have not explained yet in class. This is the `Optional' class:
This is a utility that helps our code avoid the possibility of Null Pointer Violations. We will discuss this in detail is a future class.
Try these out now. The command shell we are using has a facility for running a script - the `!rs' command. So, if you save this file somewhere as 'test.script':
cu homer simpsom homer@simpson.com secret
cu marge simpson marge@simpson.com secret
aa 0 walk fridge .001
aa 0 walk bar 1.0
aa 0 run work 2.2
aa 1 walk shop 2.5
aa 1 cycle shop 4.5
al 3 23.3 32.3
al 3 23.3 32.5
al 3 23.3 32.6
and then run it:
Welcome to pcemaker-console - ?help for instructions
pc> !rs test.script
pc$ pc$ pc$ pc$ pc$ pc$ pc$ pc$ pc$ pc$ pc> gu
[User{0, homer, simpsom, secret, homer@simpson.com, {0=Activity{0, walk, fridge, 0.001, []}, 1=Activity{1, walk, bar, 1.0, []}, 2=Activity{2, run, work, 2.2, []}}}, User{1, marge, simpson, secret, marge@simpson.com, {3=Activity{3, walk, shop, 2.5, [Location{0, 23.3, 32.3}, Location{1, 23.3, 32.5}, Location{2, 23.3, 32.6}]}, 4=Activity{4, cycle, shop, 4.5, []}}}]
pc>
Commit these changes with a suitable message
We would like our system to remember our user and activity data between sessions. In lab 1 we explored the xstream component briefly. We will reuse it here to save the entire model on exit from the application, and reload on startup.
Here is a refactoring of the main method to bring in load/store to the application life cycle:
public static void main(String[] args) throws Exception
{
Main main = new Main();
File datastore = new File("datastore.xml");
if (datastore.isFile())
{
main.paceApi.load(datastore);
}
Shell shell = ShellFactory.createConsoleShell("pm", "Welcome to pacemaker-console - ?help for instructions", main);
shell.commandLoop();
main.paceApi.store(datastore);
}
In the above main method, we check to see if the file 'datastore.xml' exists. If so, we attempt to load a model from it. If not, we proceed with a blank model.
On exit (i.e. when 'exit' is typed from the console) we ask the model to store itself.
Thus we need two new methods in PacemakerApi:
Here they are:
@SuppressWarnings("unchecked")
public 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();
}
catch(EOFException e)
{
}
finally
{
if (is != null)
{
is.close();
}
}
}
public 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.
In particular, see if you can locate the 'datastore.xml' file after the app has exited. You will need to 'refresh' your workspace for it to appear in eclipse project explorer.
If all goes to plan, and say you run the script we explored earlier, then in eclipse you should be able to examine the datastore file in an xml editor. It could look like this:
Commit these changes with a suitable message
We have implemnted an xstream based serialization of our model. However, we may be interested in revising this later, selecting a different format or even a different serialisation 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();
}
}
}
}
Now rework the Main class to attempt to load from the datastore in a constructor for Main:
public Main() throws Exception
{
File datastore = new File("datastore.xml");
Serializer serializer = new XMLSerializer(datastore);
paceApi = new PacemakerAPI(serializer);
if (datastore.isFile())
{
paceApi.load();
}
}
Note that we only load if there is a file is available. The main method can then be simplified:
public static void main(String[] args) throws Exception
{
Main main = new Main();
Shell shell = ShellFactory.createConsoleShell("pm", "Welcome to pacemaker-console - ?help for instructions", main);
shell.commandLoop();
main.paceApi.store();
}
Finally, 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(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();
}
public void store() throws Exception
{
serializer.push(userIndex);
serializer.push(emailIndex);
serializer.push(activitiesIndex);
serializer.write();
}
//...
}
You will notice that you have a lot of import warnings; remove any unused imports.
If you have an existing datastore.xml file in your project, delete this.
You should be in a position now to test. Remember, if you have a script file like this:
cu homer simpsom homer@simpson.com secret
cu marge simpson marge@simpson.com secret
aa 0 walk fridge .001
aa 0 walk bar 1.0
aa 0 run work 2.2
aa 1 walk shop 2.5
aa 1 cycle shop 4.5
al 3 23.3 32.3
al 3 23.3 32.5
al 3 23.3 32.6
Then you can just run this script using the !rs command, and the 'gu' command should print the model:
Welcome to pacemaker-console - ?help for instructions
pm> !rs test.script
pm$ pm$ pm$ pm$ pm$ pm$ pm$ pm$ pm$ pm$ pm> gu
[User{0, homer, simpsom, secret, homer@simpson.com, {0=Activity{0, walk, fridge, 0.001, []}, 1=Activity{1, walk, bar, 1.0, []}, 2=Activity{2, run, work, 2.2, []}}}, User{1, marge, simpson, secret, marge@simpson.com, {3=Activity{3, walk, shop, 2.5, [Location{0, 23.3, 32.3}, Location{1, 23.3, 32.5}, Location{2, 23.3, 32.6}]}, 4=Activity{4, cycle, shop, 4.5, []}}}]
pm>
The above session will generate the following xml file:
<object-stream>
<map>
<entry>
<long>0</long>
<models.Activity>
<id>0</id>
<type>walk</type>
<location>fridge</location>
<distance>0.001</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>1</long>
<models.Activity>
<id>1</id>
<type>walk</type>
<location>bar</location>
<distance>1.0</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>2</long>
<models.Activity>
<id>2</id>
<type>run</type>
<location>work</location>
<distance>2.2</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>3</long>
<models.Activity>
<id>3</id>
<type>walk</type>
<location>shop</location>
<distance>2.5</distance>
<route>
<models.Location>
<id>0</id>
<latitude>23.3</latitude>
<longitude>32.3</longitude>
</models.Location>
<models.Location>
<id>1</id>
<latitude>23.3</latitude>
<longitude>32.5</longitude>
</models.Location>
<models.Location>
<id>2</id>
<latitude>23.3</latitude>
<longitude>32.6</longitude>
</models.Location>
</route>
</models.Activity>
</entry>
<entry>
<long>4</long>
<models.Activity>
<id>4</id>
<type>cycle</type>
<location>shop</location>
<distance>4.5</distance>
<route/>
</models.Activity>
</entry>
</map>
<map>
<entry>
<string>homer@simpson.com</string>
<models.User>
<id>0</id>
<firstName>homer</firstName>
<lastName>simpsom</lastName>
<email>homer@simpson.com</email>
<password>secret</password>
<activities>
<entry>
<long>0</long>
<models.Activity>
<id>0</id>
<type>walk</type>
<location>fridge</location>
<distance>0.001</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>1</long>
<models.Activity>
<id>1</id>
<type>walk</type>
<location>bar</location>
<distance>1.0</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>2</long>
<models.Activity>
<id>2</id>
<type>run</type>
<location>work</location>
<distance>2.2</distance>
<route/>
</models.Activity>
</entry>
</activities>
</models.User>
</entry>
<entry>
<string>marge@simpson.com</string>
<models.User>
<id>1</id>
<firstName>marge</firstName>
<lastName>simpson</lastName>
<email>marge@simpson.com</email>
<password>secret</password>
<activities>
<entry>
<long>3</long>
<models.Activity>
<id>3</id>
<type>walk</type>
<location>shop</location>
<distance>2.5</distance>
<route>
<models.Location>
<id>0</id>
<latitude>23.3</latitude>
<longitude>32.3</longitude>
</models.Location>
<models.Location>
<id>1</id>
<latitude>23.3</latitude>
<longitude>32.5</longitude>
</models.Location>
<models.Location>
<id>2</id>
<latitude>23.3</latitude>
<longitude>32.6</longitude>
</models.Location>
</route>
</models.Activity>
</entry>
<entry>
<long>4</long>
<models.Activity>
<id>4</id>
<type>cycle</type>
<location>shop</location>
<distance>4.5</distance>
<route/>
</models.Activity>
</entry>
</activities>
</models.User>
</entry>
</map>
<map>
<entry>
<long>0</long>
<models.User>
<id>0</id>
<firstName>homer</firstName>
<lastName>simpsom</lastName>
<email>homer@simpson.com</email>
<password>secret</password>
<activities>
<entry>
<long>0</long>
<models.Activity>
<id>0</id>
<type>walk</type>
<location>fridge</location>
<distance>0.001</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>1</long>
<models.Activity>
<id>1</id>
<type>walk</type>
<location>bar</location>
<distance>1.0</distance>
<route/>
</models.Activity>
</entry>
<entry>
<long>2</long>
<models.Activity>
<id>2</id>
<type>run</type>
<location>work</location>
<distance>2.2</distance>
<route/>
</models.Activity>
</entry>
</activities>
</models.User>
</entry>
<entry>
<long>1</long>
<models.User>
<id>1</id>
<firstName>marge</firstName>
<lastName>simpson</lastName>
<email>marge@simpson.com</email>
<password>secret</password>
<activities>
<entry>
<long>3</long>
<models.Activity>
<id>3</id>
<type>walk</type>
<location>shop</location>
<distance>2.5</distance>
<route>
<models.Location>
<id>0</id>
<latitude>23.3</latitude>
<longitude>32.3</longitude>
</models.Location>
<models.Location>
<id>1</id>
<latitude>23.3</latitude>
<longitude>32.5</longitude>
</models.Location>
<models.Location>
<id>2</id>
<latitude>23.3</latitude>
<longitude>32.6</longitude>
</models.Location>
</route>
</models.Activity>
</entry>
<entry>
<long>4</long>
<models.Activity>
<id>4</id>
<type>cycle</type>
<location>shop</location>
<distance>4.5</distance>
<route/>
</models.Activity>
</entry>
</activities>
</models.User>
</entry>
</map>
</object-stream>
However - there is something profoundly problematic with the above. What is it that is wrong (it is difficult to spot)?
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:
while (!stack.empty())
{
os.writeObject(stack.pop());
}
Object obj = is.readObject();
while (obj != null)
{
stack.push(obj);
obj = is.readObject();
}
Replace the above fragments with single read/write operation:
os.writeObject(stack);
stack = (Stack) is.readObject();
This produces a different serialization - with objects shared via references. You can see this more easily from the structural view:
We have taken some time to abstract the serialization mechanism. This should enable us to introduce new serializers to adhere to alternative formats. Write two new serializers which support the following formats:
Json is supported via xstream and the jettison library. A tutorial is available on the xstream site:
A Binary serializer can be written just using the native streams in the JDK.
Run your app, or the solution made available above and list all commands:
!la
This will list a range of 'built-in' commands + the commands we have implemented so far:
abbrev name params
---------------------------------------------------
cu create-user (first name, last name, email, password)
gu get-user (email)
gu get-users ()
du delete-user (email)
aa add-activity (user-id, type, location, distance)
al add-location (activity-id, latitude, longitude)
Review the command set as specified in the assignment:
abbrev name params
---------------------------------------------------
lu list-users ()
cu create-user (first name, last name, email, password)
lu list-user (email)
lius list-user (id)
la list-activities (userid, sortBy: type, location, distance, date, duration)
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)
cff change-file-format (file format: xml, json)
l load ()
s store ()
Some commands are more or less complete:
abbrev name params
---------------------------------------------------
cu reate-user (first name, last name, email, password)
gu get-user (email)
gu get-users ()
du delete-user (email)
Some are partially complete - the activities are in place, but do not include start time or duration:
abbrev name params
---------------------------------------------------
aa add-activity (user-id, type, location, distance)
al add-location (activity-id, latitude, longitude)
and these commands are yet to be tackled:
abbrev name params
---------------------------------------------------
lius list-user (id)
la list-activities (userid, sortBy: type, location, distance, date, duration)
la list-activities (user id)
du delete-user (id)
cff change-file-format (file format: xml, json)
l load ()
s store ()
Prioritize these commands from simplest to most complex, and consider tackling the simpler commands first.