Objectives

Incorporate Activity class into the placemaker service. Extend pacemakerplaytest to exercise the new feature. Using this as a role model, in the exercises introduce Location into the service

Activities

We already have a Java version of Activity from an earlier lab:

package models;

import static com.google.common.base.Objects.toStringHelper;
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 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)
                               .toString();
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof Activity)
    {
      final Activity other = (Activity) obj;
      return Objects.equal(type, other.type) 
          && Objects.equal(location,  other.location)
          && Objects.equal(distance,  other.distance) ; 
    }
    else
    {
      return false;
    }
  }

  @Override  
  public int hashCode()  
  {  
     return Objects.hashCode(this.id, this.type, this.location, this.distance);  
  } 
}

(The route has been removed for the moment)

Adapting this to work within our Play App is reasonably straightforward.

First, remove all references to the counter - our IDs are now going to be managed by the database. Then, bring the JPA annotation @Entity, and have the class inherit from Model, and annotate our id with @GeneratedValue:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import play.db.ebean.Model;

@SuppressWarnings("serial")
@Entity
public class Activity extends Model
{
  @Id
  @GeneratedValue
  public Long   id;

We also equip the class with a Finder object:

  public static Activity findById(Long id)
  {
    return find.where().eq("id", id).findUnique();
  }
  public static Model.Finder<String, Activity> find = new Model.Finder<String, Activity>(String.class, Activity.class);

These facilitates simple typesafe model queries.

User

In our User model, a user has a list of Activities. This has been modeled as a map in and earlier version of User:

  public Map<Long, Activity> activities = new HashMap<>();

We can revert to slightly simpler model in Play - and take advantage of the framework to manage this relationship efficiently. Insert the following into the User class:

  @OneToMany(cascade=CascadeType.ALL)
  public List<Activity> activities = new ArrayList<Activity>();

The OneToMany annotation will ensure that the generated database will accommodate this relationship. We have implemented it in unidirectional manner - the Users know about the activity instances, but not the other way around (We may wish to revisit this later).

See

for a general introduction as to how this relationship is realised in a database.

Json Support

When we wish to expose aspects of our model, we need Json support. Here is a revised version of the JsonParser to accommodate the Activity class:

public class JsonParser
{
  private static JSONSerializer  userSerializer     = new JSONSerializer().exclude("class");
  private static JSONSerializer  activitySerializer = new JSONSerializer().exclude("class");

  public static User renderUser(String json)
  {
    return new JSONDeserializer<User>().deserialize(json, User.class); 
  }

  public static String renderUser(Object obj)
  {
    return userSerializer.serialize(obj);
  }

  public static List<User> renderUsers(String json)
  {
    return new JSONDeserializer<ArrayList<User>>().use("values", User.class).deserialize(json);
  }   

  public static Activity renderActivity(String json)
  {
    Activity activity = new JSONDeserializer<Activity>().deserialize(json,   Activity.class);
    return activity;
  }

  public static String renderActivity(Object obj)
  {
    return activitySerializer.serialize(obj);
  }

  public static  List<Activity> renderActivities (String json)
  {
    return new JSONDeserializer<ArrayList<Activity>>().use("values", Activity.class).deserialize(json);
  }  
}

API

These are additional API methods that can be appended to PacemakerAPI to support Activity API access:

  public static Result activities (Long userId)
  {  
    User p = User.findById(userId);
    return ok(renderActivity(p.activities));
  }

  public static Result createActivity (Long userId)
  { 
    User    user      = User.findById(userId);
    Activity activity = renderActivity(request().body().asJson().toString());  

    user.activities.add(activity);
    user.save();

    return ok(renderActivity(activity));
  }

  public static Result activity (Long userId, Long activityId)
  {  
    User    user      = User.findById(userId);
    Activity activity = Activity.findById(activityId);

    if (activity == null)
    {
      return notFound();
    }
    else
    {
      return user.activities.contains(activity)? ok(renderActivity(activity)): badRequest();
    }
  }  

  public static Result deleteActivity (Long userId, Long activityId)
  {  
    User    user      = User.findById(userId);
    Activity activity = Activity.findById(activityId);
    if (activity == null)
    {
      return notFound();
    }
    else
    {
      if (user.activities.contains(activity))
      {
        user.activities.remove(activity);
        activity.delete();
        user.save();
        return ok();
      }
      else
      {
        return badRequest();
      }

    }
  }  

  public static Result updateActivity (Long userId, Long activityId)
  {
    User    user      = User.findById(userId);
    Activity activity = Activity.findById(activityId);
    if (activity == null)
    {
      return notFound();
    }
    else
    {
      if (user.activities.contains(activity))
      {
        Activity updatedActivity = renderActivity(request().body().asJson().toString());
        activity.distance = updatedActivity.distance;
        activity.location = updatedActivity.location;
        activity.type     = updatedActivity.type;

        activity.save();
        return ok(renderActivity(updatedActivity));
      }
      else
      {
        return badRequest();
      }
    }
  }   

These features are exposed by additional routes in conf/routes:

GET     /api/users/:userId/activities              controllers.PacemakerAPI.activities(userId: Long)
POST    /api/users/:userId/activities              controllers.PacemakerAPI.createActivity(userId: Long)

GET     /api/users/:userId/activities/:activityId  controllers.PacemakerAPI.activity(userId: Long, activityId:Long)
DELETE  /api/users/:userId/activities/:activityId  controllers.PacemakerAPI.deleteActivity(userId: Long, activityId:Long)
PUT     /api/users/:userId/activities/:activityId  controllers.PacemakerAPI.updateActivity(userId: Long, activityId:Long)

NB: The routes file is compiled - and can generate syntax errors if incorrectly formed. This compilation is not integrated into eclipse, so you will need to run 'compile' in the play console if it is to be checked before running.

pacemakerplayeest - User and JsonParser

We can now turn our attention to the pacemakerplaytest - and see if we can exercise these new APIs.

The test project will need a simpified version of the Activity class:

ackage app.models;

import static com.google.common.base.Objects.toStringHelper;
import com.google.common.base.Objects;

public class Activity
{
  public Long   id;
  public String type;
  public String location;
  public double distance;

  public Activity()
  {
  }

  public Activity(String type, String location, double distance)
  {
    this.type      = type;
    this.location  = location;
    this.distance  = distance;
  }

  @Override
  public String toString()
  {
    return toStringHelper(this).addValue(id)
                               .addValue(type)
                               .addValue(location)
                               .addValue(distance)
                               .toString();
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof Activity)
    {
      final Activity other = (Activity) obj;
      return Objects.equal(type, other.type) 
          && Objects.equal(location,  other.location)
          && Objects.equal(distance,  other.distance) ; 
    }
    else
    {
      return false;
    }
  }

  @Override  
  public int hashCode()  
  {  
     return Objects.hashCode(this.id, this.type, this.location, this.distance);  
  } 
}

... and also the JsonParser Class from the play project. It will be identical, except for the import statements:

package app.api;

import java.util.ArrayList;
import java.util.List;

import app.models.Activity;
import app.models.User;
import flexjson.JSONDeserializer;
import flexjson.JSONSerializer;

public class JsonParser
{
  private static JSONSerializer  userSerializer     = new JSONSerializer().exclude("class");
  private static JSONSerializer  activitySerializer = new JSONSerializer().exclude("class");

  public static User renderUser(String json)
  {
    return new JSONDeserializer<User>().deserialize(json, User.class); 
  }

  public static String renderUser(Object obj)
  {
    return userSerializer.serialize(obj);
  }

  public static List<User> renderUsers(String json)
  {
    return new JSONDeserializer<ArrayList<User>>().use("values", User.class).deserialize(json);
  }   

  public static Activity renderActivity(String json)
  {
    Activity activity = new JSONDeserializer<Activity>().deserialize(json,   Activity.class);
    return activity;
  }

  public static String renderActivity(Object obj)
  {
    return activitySerializer.serialize(obj);
  }

  public static  List<Activity> renderActivities (String json)
  {
    return new JSONDeserializer<ArrayList<Activity>>().use("values", Activity.class).deserialize(json);
  }  
}

These two classes should compile without error.

pacemakerplaytest - api

The PacemakerAPI class in pacemakerplaytest is already equipped with convenience methods to manage users. We can supplement those with activity management methods:

  public static Activity createActivity(Long userId, String activityJson) throws Exception
  {
    String response = Rest.post("/api/users/" + userId + "/activities", activityJson);
    return JsonParser.renderActivity(response);
  }

  public static Activity createActivity(Long userId, Activity activity) throws Exception
  {
    return createActivity(userId, JsonParser.renderActivity(activity));
  }

  public static Activity getActivity(Long userId, Long activityId) throws Exception
  {
    String response = Rest.get("/api/users/" + userId + "/activities/" + activityId);
    Activity activity = JsonParser.renderActivity(response);
    return activity;
  }

  public static Activity updateActivity(Long userId, Long aid, Activity activity) throws Exception
  {
    return updateActivity(userId, aid, JsonParser.renderActivity(activity));
  }

  public static Activity updateActivity(Long userId, Long aid, String activityJson) throws Exception
  {
    String response = Rest.put("/api/users/" + userId + "/activities/" + aid, activityJson);
    return JsonParser.renderActivity(response);
  }

  public static List<Activity> getActivities(Long userId) throws Exception
  {
    String response = Rest.get("/api/users/" + userId + "/activities");
    return JsonParser.renderActivities(response);
  }  

  public static void  deleteActivity(Long userId, Long activityId) throws Exception
  {  
    Rest.delete("/api/users/" + userId + "/activities/" + activityId);
  } 

Look at these carefully - there are a number of variations on each call, some entirely json, some taking Java objects, which call the Json variants.

Fixtures

We can supplement our Fixtures with an sample Activity Json object:

  static String activityJson  = "{\n"
                                    +  "\"type\"      : \"run\"                 ,\n"
                                    +  "\"location\"  : \"Dunmore\"             ,\n"
                                    +  "\"distance\"  : 3                        \n"
                               + "}";

Activity Tests

This is a set of tests to stimulate the Activities API:

package app.test;

import static org.junit.Assert.*;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import app.api.PacemakerAPI;
import app.api.Rest;
import app.models.Activity;
import app.models.User;

public class ActivityTest
{
  private User     user;
  private Activity activity;

  @Before
  public void setUp() throws Exception
  {
    user              = new User ("mark", "simpson", "mark@simpson.com", "secret");
    activity          = new Activity ("run",  "tramore", 3);

    user = PacemakerAPI.createUser(Fixtures.userJson);
  }

  @After
  public void tearDown() throws Exception
  {
    Rest.delete("/api/users");
  }

  @Test
  public void createActivityJson() throws Exception
  {      
    Activity activity  = PacemakerAPI.createActivity(user.id, Fixtures.activityJson);
    Activity getResponse = PacemakerAPI.getActivity(user.id, activity.id);

    assertTrue(activity.equals(getResponse));

    PacemakerAPI.deleteActivity(user.id, activity.id);
  }

  @Test
  public void createActivityObj() throws Exception
  { 
    Activity createResponse = PacemakerAPI.createActivity(user.id, activity);
    assertEquals(activity, createResponse);

    Activity getResponse = PacemakerAPI.getActivity(user.id, createResponse.id);
    assertEquals(activity, getResponse);  

    PacemakerAPI.deleteActivity(user.id, createResponse.id);
  }


  @Test
  public void updateActivity() throws Exception
  { 
    Activity createResponse = PacemakerAPI.createActivity(user.id, activity);
    assertEquals(activity, createResponse);

    activity.distance = 12;
    activity.location = "connemara";
    Activity updateResponse = PacemakerAPI.updateActivity(user.id, createResponse.id, activity);
    assertEquals(activity, updateResponse);  

    PacemakerAPI.deleteActivity(user.id, createResponse.id);
  }


  @Test
  public void getActivities() throws Exception
  { 
    Activity createResponse = PacemakerAPI.createActivity(user.id, activity);

    List<Activity> activities = PacemakerAPI.getActivities(user.id);
    assertEquals(1, activities.size());

    assertEquals(activity, activities.get(0));
    PacemakerAPI.deleteActivity(user.id, createResponse.id);
  }


  @Test
  public void getMultipleActivities() throws Exception
  { 
    Activity createResponse = PacemakerAPI.createActivity(user.id, activity);

    Activity activity2  = new Activity ("ride",  "tramore", 3);
    Activity createResponse2 = PacemakerAPI.createActivity(user.id, activity2);

    List<Activity> activities = PacemakerAPI.getActivities(user.id);
    assertEquals(2, activities.size());

    assertEquals(activity, activities.get(0));
    assertEquals(activity2, activities.get(1));
    PacemakerAPI.deleteActivity(user.id, createResponse.id);
    PacemakerAPI.deleteActivity(user.id, createResponse2.id);
  }
}

They should all pass

Exercises: Location

Using the User/Activity classes as a role model, consider introducing the Location class into the model. This is a revision of the Location class from our console app:

package models;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import play.db.ebean.Model;

import com.google.common.base.Objects;

@Entity
public class Location extends Model
{
  @Id
  @GeneratedValue
  public Long id;

  public float latitude;
  public float longitude;

  public Location()
  {
  }

  public Location (float latitude, float longitude)
  {
    this.latitude = latitude;
    this.longitude = longitude;
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof Location)
    {
      final Location other = (Location) obj;
      return Objects.equal(latitude, other.latitude) 
          && Objects.equal(longitude, other.longitude);
    }
    else
    {
      return false;
    }
  }

  public String toString()
  {
    return Objects.toStringHelper(this)
        .add("Latitude", latitude)
        .add("Longitude", longitude).toString();
  }

This class should not need further work for the moment.

Exercise 1: Activity / Location

Introduce a OneToMany relationship from Activity to Location to realise routes from our console app.

Exercise 2: JsonParser

Extend the JsonParser classe to accommodate the Location class transformation

Exercise 3: Routes

Agument the routes to facilitate route create/read/update/delete by providing new routes using POST/GET/PUT/DELETE verbs

Exercise 4: Tests

Introduce new tests into the playpacemakertest class to exercise the new location feature