Objectives

Create xtend project

Create a new eclipse java project called pacemaker-console-x.

Initialize the project as a git repository (see lab 2) - and replace the generated .gitignore file with this one here:

# Ignore all dotfiles...
.*
# except for .gitignore
!.gitignore
!.classpath
!.project

*.java
*.xml


/bin
./xtend-gen/**
.settings

Now commit the project - with a simple message:

Your blank workspace will look like this:

In src, create a new package called 'models'. Then, in this package, create a new xtend class called 'User'. Xrend options should be available from the 'other' category:

The generated class will trigger errors. Fix these using autocorrect -

This should reconfigure workspace as follows:

If the error persists in the editor - then close and reopen the file.

Now we will try or first xtend class:

@Data class User 
{
  Long   id
  String firstname
  String lastname 
  String email
  String password
}

Replace the blank User with the above and save. Some interesting things will happen to the workspace:

Have a look at the new java version of our User class:

package models;

import org.eclipse.xtend.lib.Data;
import org.eclipse.xtext.xbase.lib.util.ToStringHelper;

@Data
@SuppressWarnings("all")
public class User {
  private final Long _id;

  public Long getId() {
    return this._id;
  }

  private final String _firstname;

  public String getFirstname() {
    return this._firstname;
  }

  private final String _lastname;

  public String getLastname() {
    return this._lastname;
  }

  private final String _email;

  public String getEmail() {
    return this._email;
  }

  private final String _password;

  public String getPassword() {
    return this._password;
  }

  public User(final Long id, final String firstname, final String lastname, final String email, final String password) {
    super();
    this._id = id;
    this._firstname = firstname;
    this._lastname = lastname;
    this._email = email;
    this._password = password;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((_id== null) ? 0 : _id.hashCode());
    result = prime * result + ((_firstname== null) ? 0 : _firstname.hashCode());
    result = prime * result + ((_lastname== null) ? 0 : _lastname.hashCode());
    result = prime * result + ((_email== null) ? 0 : _email.hashCode());
    result = prime * result + ((_password== null) ? 0 : _password.hashCode());
    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    User other = (User) obj;
    if (_id == null) {
      if (other._id != null)
        return false;
    } else if (!_id.equals(other._id))
      return false;
    if (_firstname == null) {
      if (other._firstname != null)
        return false;
    } else if (!_firstname.equals(other._firstname))
      return false;
    if (_lastname == null) {
      if (other._lastname != null)
        return false;
    } else if (!_lastname.equals(other._lastname))
      return false;
    if (_email == null) {
      if (other._email != null)
        return false;
    } else if (!_email.equals(other._email))
      return false;
    if (_password == null) {
      if (other._password != null)
        return false;
    } else if (!_password.equals(other._password))
      return false;
    return true;
  }

  @Override
  public String toString() {
    String result = new ToStringHelper().toString(this);
    return result;
  }
}

This have been generated by eclipse - and is compiled by the standard java compiler.

Commit these changes to git. Note that our .gitignore means that we will not be offered an opportunity to commit the java source. This is intentional - as we are happy to work in xtend only.

Incorporate Junit

The next step will be to create a unit test to verify our understanding of the Xtend class. Before doing this, add JUnit4 as a library for the project (Project->Properties->Build Path->Libraries->Add Library)

In Eclipse, create a new 'source folder' called 'test' in the root of the project. In that folder create an new package called models:

Create a new xtend class called 'UserTest' in this package, and incorporate the following:

package models

import static org.junit.Assert.*
import org.junit.Test
import org.junit.Before
import org.junit.After

class UserTest 
{
   val userStr = 'User [
  _id = 0
  _firstname = "homer"
  _lastname = "simpson"
  _email = "homer@simpson.com"
  _password = "secret"
]' 

  var User homer 

  @Before
  def void setup()
  {
    homer = new User(0l, "homer", "simpson", "homer@simpson.com",  "secret")
  }

  @After def void tearDown()
  {
    homer = null
  }

  @Test def void testCreate()
  {
    assertEquals ("homer",               homer.firstname)
    assertEquals ("simpson",             homer.lastname)
    assertEquals ("homer@simpson.com",   homer.email) 
    assertEquals ("secret",              homer.password)  
  }

  @Test def void testToString()
  {
    assertEquals (userStr, homer.toString)
  }
}

This test should pass - and verifies our understanding of how simple @Data classes in xtend function. We might try another simple test:

  @Test def void testEquals()
  {
   val anotherHomer = new User(0l, "homer", "simpson", "homer@simpson.com",  "secret")
   val marge        = new User(0l, "marge", "simpson", "marge@simpson.com",  "secret")

   assertEquals   (homer, anotherHomer)
   assertNotSame  (homer, anotherHomer)
   assertNotEquals(homer, marge)
  }

This verifies that the generated equals() method works as expected. Commit these changes with a suitable message.

@Data class

Create a new model class called Activity:

@Data class Activity 
{
  Long     id
  String   type
  String   location
  double   distance
}

and a new test to exercise it:

package models

import static org.junit.Assert.*
import org.junit.Test
import org.junit.Before
import org.junit.After

class ActivityTest
{
  val activityStr = 'Activity [
  _id = 0
  _type = "walk"
  _location = "fridge"
  _distance = 0.001
]' 

  var Activity activity 

  @Before
  def void setup()
  {
    activity = new Activity (0l, "walk",  "fridge", 0.001)
  }

  @After def void tearDown()
  {
    activity = null
  }

  @Test def void testCreate()
  {
    assertEquals ("walk",          activity.type)
    assertEquals ("fridge",        activity.location)
    assertEquals (0.0001, 0.001,   activity.distance)    
  }  

  @Test def void testToString()
  {
    assertEquals (activityStr, activity.toString)
  }
}

This test should pass. There is no need to test the equals() method, as we have verified this basic operation for User.

Map as Property

We now establish the relationship between users and activities - introduce a new attribute to the User class:

  @Property Map<Long, Activity> activities = new HashMap

(user autocorrect to bring in the appropriate includes)

Run our unit tests - the toString test for User will fail immediately. This is because our generated toString method is now also rendering the activities attribute. Change the test fixture to accommodate this:

   val userStr = 'User [
  _id = 0
  _firstname = "homer"
  _lastname = "simpson"
  _email = "homer@simpson.com"
  _password = "secret"
  _activities = {}
]' 

The test should now succeed.

We can complete the model classes now - introduce a new Location class:

@Data class Location
{
  float latitude 
  float longitude 
}

and in Activity, establish the relationship to Location:

  @Property List<Location> route = new ArrayList

As with UserTest, the ActivityTest will fail - so we adjust the fixture to include the new reference to Locations:

  val activityStr = 'Activity [
  _id = 0
  _type = "walk"
  _location = "fridge"
  _distance = 0.001
  _route = ArrayList ()
]' 

The tests should now pass.

For the moment we choose not to introduce tests for Location - as the class is so simple we woule merely be testing the generated code. This is unnessasry, we the tests we have written so far should be enough to confirm our understanding of how these @Data Xrend classes work.

Commmit the change made so far.

PacemakerAPI

Create a new model called 'controllers' and a class therein called "PacemakerAPI". Mirror this with a corresponding test class:

We will try writing the unit tests slightly ahead of the class under test. In the pacemakerAPITest, lets bring in the basic test support:

package controllers

import static org.junit.Assert.*
import org.junit.Test
import org.junit.Before
import org.junit.After
import controllers.PacemakerAPI
import models.User
import models.Location

class PacemakerAPITest
{
  @Before
  def void setup()
  {
  }

  @After def void tearDown()
  {
  }
}

Now create a fixture:

  var PacemakerAPI pacemakerAPI

  @Before
  def void setup()
  {
    pacemakerAPI = new PacemakerAPI()
  }

  @After def void tearDown()
  {
    pacemakerAPI = null
  }

... and a new test:

  @Test def void createUser()
  {
    val homer    = new User(1l, "homer", "simpson", "homer@simpson.com",  "secret")

    assertEquals (0, pacemakerAPI.users.size)
    val id = pacemakerAPI.createUser("homer", "simpson", "homer@simpson.com", "secret")
    assertEquals (1, pacemakerAPI.users.size)
    assertEquals (homer, pacemakerAPI.getUser(id))
  }

This will fail to compile - as the PacemakerAPI class is currently blank.

We can try the absolute minimum to get the test to compile - and no more. i.e. we would like the test to compile - and then fail when run. Here is a version of PacemakerAPI that will at least compile:

package controllers

import models.User
import java.util.Collection

class PacemakerAPI 
{
  @Property Collection<User> users

  def Long createUser (String firstName, String lastName, String email, String password) 
  { 
  }

  def getUser (Long id)
  {
  }     
}

Try the test now - and verify that it compiles successfully, although fails to execute.

The following version of PacemakerAPI should pass:

class PacemakerAPI 
{
  static long userIndex = 0

  val Map<Long,   User>      userMap = new HashMap
  @Property Collection<User> users   = userMap.values

  def createUser (String firstName, String lastName, String email, String password) 
  { 
    userIndex = userIndex + 1
    userMap.put(userIndex, new User (userIndex, firstName, lastName, email, password))
    userIndex
  }

  def getUser (Long id)
  {
    userMap.get(id)
  }     
}

Note the subtle change in the signature for createUser.

Commit these changes.

PacemakerAPITest

Introduce some test data into PacemakerAPI class we can use to exercise the api more throughly:

  public static val users = newArrayList
  ( new User (1l, "marge", "simpson", "marge@simpson.com",  "secret"),
    new User (2l, "lisa",  "simpson", "lisa@simpson.com",   "secret"),
    new User (3l, "bart",  "simpson", "bart@simpson.com",   "secret"),
    new User (4l, "maggie","simpson", "maggie@simpson.com", "secret") )  

Now the setup method can create 4 users:

  @Before
  def void setup()
  {
    pacemakerAPI = new PacemakerAPI()
    users.forEach [pacemakerAPI.createUser(firstname, lastname, email, password)]
  }

This will require a slight change to the existing test:

  @Test def void createUser()
  {
    val homer    = new User(5L, "homer", "simpson", "homer@simpson.com",  "secret")

    assertEquals (users.size, pacemakerAPI.users.size)
    val id = pacemakerAPI.createUser("homer", "simpson", "homer@simpson.com", "secret")
    assertEquals (users.size+1, pacemakerAPI.users.size)
    assertEquals (homer, pacemakerAPI.getUser(id))
  }

... and we can introduce a more complete test of addActivity:

  @Test
  def void testUsers()
  {
    assertEquals (users.size, pacemakerAPI.users.size)

    assertEquals (users.size, pacemakerAPI.users.size)
    users.forEach [ val eachUser = pacemakerAPI.getUser(email)
                    assertEquals (it, eachUser)
                    assertNotSame(it, eachUser) ] 
  } 

This last test will fail to compile, as we have yet to implement getUser taking an email.

Here is a rework of PacemakerAPI to accommodate the a new email index:

class PacemakerAPI 
{
  static long userIndex = 0

  val Map<Long,   User>      userMap = new HashMap
  val Map<String, User>      userEmailMap  = new HashMap  
  @Property Collection<User> users   = userMap.values

  def createUser (String firstName, String lastName, String email, String password) 
  { 
    userIndex = userIndex + 1
    val user = new User (userIndex, firstName, lastName, email, password)
    userMap.put(userIndex, user)
    userEmailMap.put(user.email, user)
    userIndex
  }

  def getUser (Long id)
  {
    userMap.get(id)
  }  

  def getUser (String email)
  {
    userEmailMap.get(email)
  }  

Run the tests again - we still have a fail, but this time back in the test we thought we had concluded. Why is this?

The fix is to intoduce a constructor into PacemakerAPI to reset the index to 0 every time we create a PacemakerAPI object:

  new()
  {
    userIndex = 0
  }

Test this again, it should pass.

We should also introduce ability yo delete users. First, the test:

  @Test def void testDeleteUsers()
  {
    assertEquals (users.length, pacemakerAPI.users.size)
    val user = pacemakerAPI.getUser("marge@simpson.com")
    pacemakerAPI.deleteUser(user.id)
    assertEquals (users.length-1, pacemakerAPI.users.size)  
  }

.. and the implementation:

  def deleteUser (Long id)
  {
    userEmailMap.remove(userMap.get(id))
    userMap.remove(id)
  } 

All test should pass.

Commit changes.

Activity Support

The following three methods in PacemakerAPI should round of the features we need:

  def Activity createActivity(Long id, String type, String location, double distance)
  {
  }

  def Activity getActivity (Long id)
  {
  }

  def void addLocation (Long id, float latitude, float longitude)
  {
  }

These are how we might articulate the tests:

  @Test def void testAddActivity()
  {
    val activity = pacemakerAPI.createActivity(pacemakerAPI.getUser("marge@simpson.com").id,  "run", "springfield", 5)

    val returnedActivity = pacemakerAPI.getActivity(activity.id);
    assertEquals(activity,  returnedActivity);
  }   

  @Test def void testAddActivityWithSingleLocation()
  {
    val activity = pacemakerAPI.createActivity(pacemakerAPI.getUser("marge@simpson.com").id,  "run", "springfield", 5)

    val location = new Location(23.3f, 33.3f)

    val returnedActivity = pacemakerAPI.getActivity(activity.id)

    pacemakerAPI.addLocation(returnedActivity.id, location.latitude, location.longitude)

    val otherActivity = pacemakerAPI.getActivity(returnedActivity.id)
    assertEquals (1, otherActivity.route.size)
    assertEquals(0.0001, location.latitude,  otherActivity.route.get(0).latitude)
    assertEquals(0.0001, location.longitude, otherActivity.route.get(0).longitude);  
  }   

  @Test def void testAddActivityWithMultipleLocation()
  {
    val locations = newArrayList
     (
      new Location(23.3f, 33.3f),
      new Location(34.4f, 45.2f),  
      new Location(25.3f, 34.3f),
      new Location(44.4f, 23.3f)       
     )

    val activity = pacemakerAPI.createActivity(pacemakerAPI.getUser("marge@simpson.com").id,  "run", "springfield", 5)

    val returnedActivity = pacemakerAPI.getActivity(activity.id)

    locations.forEach[ pacemakerAPI.addLocation(returnedActivity.id, latitude, longitude)]    

    val otherActivity = pacemakerAPI.getActivity(returnedActivity.id)
    assertEquals (locations.size, otherActivity.route.size)
    assertEquals (locations, otherActivity.route)
   }

These tests are reasonably exhaustive. Now to implement activities in PacemakerAPI.

First, a revision of the key attributes:

class PacemakerAPI 
{
  static long userIndex     = 0
  static long activityIndex = 0

  val Map<Long,   User>      userMap       = new HashMap
  val Map<String, User>      userEmailMap  = new HashMap  
  var Map<Long,   Activity>  activityMap   = new HashMap  
  @Property Collection<User> users         = userMap.values

  new()
  {
    userIndex     = 0
    activityIndex = 0
  }

... and here is the implementation of the new methods stubbed earlier:

  def Activity createActivity(Long id, String type, String location, double distance)
  {
    var Activity activity = null;
    var user = userMap.get(id)
    if (null != user)
    {
      activityIndex = activityIndex + 1
      activity = new Activity (activityIndex, type, location, distance)
      user.activities.put(activity.id, activity);
      activityMap.put(activity.id, activity);
    }
    return activity;
  }

  def getActivity (Long id)
  {
    activityMap.get(id)
  }

  def void addLocation (Long id, float latitude, float longitude)
  {
    val activity = activityMap.get(id)
    if (null != activity)
    {
      activity.route.add(new Location(latitude, longitude));
    }
  }

We now have a reasonably comprehensive version of the api implemented. More importantly, we have a robust and expressive set of tests which will stand to our benefit as we introduce new features and capabilities.

Serializers

The final test sequence is to verify correct implementation of the persistence mechanism. This will be implemented in a new package called utils.

Create this package now, and incorporate the following interface:

package utils

interface Serializer 
{
  def void push(Object o)
  def Object pop()
  def void write()
  def void read()
}

In the same package, introduce this implementation of Serializer:

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.Deque
import java.util.ArrayDeque

class XMLSerializer implements Serializer
{
  var Deque<Object> stack = new ArrayDeque
  val File file;

  new (File file)
  {
    this.file = file
  }

  def override void push(Object o)
  {
    stack.push(o)
  }

  def override Object pop()
  {
    return stack.pop();
  }

  @SuppressWarnings("unchecked")
  def override void read() 
  {
    var ObjectInputStream is = null

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

  def override void write() 
  {
    var ObjectOutputStream os = null

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

This will require the thoughtworks xtream library to be added to the project.

Now the test - we create a separate test just focusing on persistence:

package controllers

import static org.junit.Assert.*
import org.junit.Test
import org.junit.Before
import org.junit.After
import controllers.PacemakerAPI
import java.io.File
import utils.XMLSerializer
import utils.Serializer
import models.User
import models.Activity
import models.Location

class PersistenceTest 
{
  val users = newArrayList
  ( new User (1l, "marge", "simpson", "marge@simpson.com",  "secret"),
    new User (2l, "lisa",  "simpson", "lisa@simpson.com",   "secret"),
    new User (3l, "bart",  "simpson", "bart@simpson.com",   "secret"),
    new User (4l, "maggie","simpson", "maggie@simpson.com", "secret") )  

  val margesActivities = newArrayList
  ( new Activity (1l, "walk",  "fridge", 0.001),
    new Activity (2l, "walk",  "bar",    1.0),
    new Activity (3l, "run",   "work",   2.2))

  val lisasActivities = newArrayList  
  ( new Activity (4l, "walk",  "shop",   2.5),
    new Activity (5l, "cycle", "school", 4.5) )       

  public val locations = newArrayList
  ( new Location(23.3f, 33.3f),
    new Location(34.4f, 45.2f),  
    new Location(25.3f, 34.3f),
    new Location(44.4f, 23.3f) )    

  var PacemakerAPI pacemaker
  var Serializer   serializer
  val datastoreFile = "testdatastore.xml";

  def void deleteFile(String fileName)
  {
    val datastore = new File (fileName)
    if (datastore.exists)
    {
      datastore.delete
    }
  }

  def void populate()
  {
    users.forEach [pacemaker.createUser(firstname, lastname, email, password)] 

    val marge = pacemaker.getUser("marge@simpson.com")
    margesActivities.forEach[ pacemaker.createActivity(marge.id, type, location, distance) ]

    locations.forEach[ pacemaker.addLocation(marge.activities.values.get(0).id, latitude, longitude)]

    val lisa = pacemaker.getUser("lisa@simpson.com")
    lisasActivities.forEach[ pacemaker.createActivity(lisa.id, type, location, distance) ]
  }

  @Before
  def void setup()
  {
    deleteFile (datastoreFile);
    serializer = new XMLSerializer(new File (datastoreFile))
    pacemaker = new PacemakerAPI(serializer) 
    populate
  }

  @After
  def void tearDown()
  {
    pacemaker = null
    serializer = null
    deleteFile (datastoreFile);
  }

  @Test def void testXMLSerializer() 
  { 
    pacemaker.store
    val pacemaker2 =  new PacemakerAPI(serializer)
    pacemaker2.load
    pacemaker.users.forEach [assertTrue(pacemaker2.users.contains(it))]
  }
}

This requires new attributes and methods in PacemakerAPI:

class PacemakerAPI 
{
  static long userIndex     = 0
  static long activityIndex = 0

  var Map<Long,   User>      userMap       = new HashMap
  var Map<String, User>      userEmailMap  = new HashMap  
  var Map<Long,   Activity>  activityMap   = new HashMap  
  @Property Collection<User> users         = userMap.values
  var Serializer             serializer

  new()
  {
    userIndex     = 0
    activityIndex = 0
  }

  new (Serializer serializer)
  {
    this.serializer = serializer
    userIndex       = 0
    activityIndex   = 0
  }

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

  def void store() 
  {
    serializer.push(userMap)
    serializer.push(userEmailMap)
    serializer.push(activityMap)
    serializer.push(userIndex)
    serializer.push(activityIndex)
    serializer.write()
  }

  //... as before

All tests should new pass.

Exercises

The project so far:

Exploring the XML File

You can manually explore the generated xml file be commenting out the deletion of the file in the tearDown of PersistenceTest:

  @After
  def void tearDown()
  {
    pacemaker = null
    serializer = null
    //deleteFile (datastoreFile);
  }

Try this and see if you can relate the generated file to the unit test fixtures

User Interface

Missing form this solution is the User Interface. We already implemented this in Java. Convert this to Xtend. Use the generated xml file as the default model when the application is launched. See if the user/activities/locations are accurately restored.

Json Serializer

If you have made progress your JSon or Binary serializers, convert them to Xtend and write tests to verify their operation.