Objectives

Refactor the the Donation Play Service to capture a OneToMany relationship from User->Donation. Then, reflect this relationship in the API.

Routes

We will start with the routes. In the previous version the User and Donations APIs were independent. In the revised routes below, not carefully how the Donations routes are structured.

# API - Users

GET     /api/users                           UsersAPI.users
GET     /api/users/{id}                      UsersAPI.user
POST    /api/users                           UsersAPI.createUser
DELETE  /api/users/{id}                      UsersAPI.deleteUser

# API - Donations

GET     /api/users/{userId}/donations        DonationsAPI.donations
GET     /api/users/{userId}/donations/{id}   DonationsAPI.donation
POST    /api/users/{userId}/donations        DonationsAPI.createDonation
DELETE  /api/users/{userId}/donations/{id}   DonationsAPI.deleteDonation

As you can see, the users ID is required in order to reach the donations. The returned (or created) donations will be associated with the given user id.

User Model

The User Model can now incorporate the relationship:

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

We can keep it unidirectional for the moment.

Controllers

We can get rid of the current controller, and introduce these two new controllers instead.

This one looks after the User model:

UsersAPI

package controllers;

import play.*;
import play.mvc.*;
import utils.JsonParsers;

import java.util.*;

import com.google.gson.JsonElement;

import models.*;

public class UsersAPI extends Controller
{
  public static void users()
  {
    List<User> users = User.findAll();
    renderJSON(JsonParsers.user2Json(users));
  }

  public static void user(Long id)
  {
    User user = User.findById(id);  
    if (user == null)
    {
      notFound();
    }
    else
    {
      renderJSON(JsonParsers.user2Json(user));
    }
  }

  public static void createUser(JsonElement body)
  {
    User user = JsonParsers.json2User(body.toString());
    user.save();
    renderJSON(JsonParsers.user2Json(user));
  }

  public static void deleteUser(Long id)
  {
    User user = User.findById(id);
    if (user == null)
    {
      notFound();
    }
    else
    {
      user.delete();
      renderText("success");
    }
  }

  public static void deleteAllUsers()
  {
    User.deleteAll();
    renderText("success");
  }      
}

.. and this is where we manage the Donations model:

DonationsAPI

package controllers;

import play.*;
import play.mvc.*;
import utils.JsonParsers;

import java.util.*;

import com.google.gson.JsonElement;

import models.*;

public class DonationsAPI extends Controller
{
  public static void donations(Long userId)
  {
    User user = User.findById(userId);
    List<Donation> donations = user.donations;
    renderText(JsonParsers.donation2Json(donations));
  }

  public static void donation (Long userId, Long id)
  {
    User user = User.findById(userId);
    Donation donation = Donation.findById(id);
    if (user.donations.contains(donation))
    { 
      renderJSON (JsonParsers.donation2Json(donation));
    }
    else
    {
      badRequest();
    }
  }

  public static void createDonation(Long userId, JsonElement body)
  {
    User user = User.findById(userId);
    Donation donation = JsonParsers.json2Donation(body.toString());
    Donation newDonation = new Donation (donation.amount, donation.method);
    user.donations.add(newDonation);
    user.save();
    renderJSON (JsonParsers.donation2Json(newDonation));
  }  

  public static void deleteDonation(Long userId, Long id)
  {
    User user = User.findById(userId);
    Donation donation = Donation.findById(id);
    if (!user.donations.contains(donation))
    {
      notFound();
    }
    else
    {
      user.donations.remove(donation);
      user.save();
      donation.delete();
      ok();
    }
  }  
}

In the above you will see we are always using the user ID.

Test Project - DonationServiceAPI

We have just completed a significant overhaul to the service API - so the tests will need to change to reflect this.

Open the donation-service-test project - and we can extend the API to accommodate the donations:

DonationServiceAPI

  public static List<Donation> getDonations(User user) throws Exception
  {
    String response =  Rest.get("/api/users/" + user.id + "/donations");
    List<Donation> donationList = JsonParsers.json2Donations(response);
    return donationList;
  }

  public static Donation createDonation(User user, Donation donation) throws Exception
  {
    String response = Rest.post ("/api/users/" + user.id + "/donations", JsonParsers.donation2Json(donation));
    return JsonParsers.json2Donation(response);
  }

  public static void deleteDonation(User user, Donation donation) throws Exception
  {
    Rest.delete ("/api/users/" + user.id + "/donations/" + donation.id);
  } 

Test Project - DonationTest

The existing user tests should still work.

However, we can introduce this new DonationTest class to exhaustively test the revised Donations API:

DonationTest

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.DonationServiceAPI;
import app.model.Donation;
import app.model.User;

public class DonationTest
{
  User marge =  new User ("marge",  "simpson", "homer@simpson.com",  "secret");

  @Before
  public void setup() throws Exception
  { 
    marge = DonationServiceAPI.createUser(marge);
  }

  @After
  public void teardown() throws Exception
  {
    DonationServiceAPI.deleteUser(marge);
  }

  @Test
  public void testCreateDonation () throws Exception
  {
    Donation donation = new Donation (123, "cash");
    Donation returnedDonation = DonationServiceAPI.createDonation(marge, donation);
    assertEquals (donation, returnedDonation);

    DonationServiceAPI.deleteDonation(marge, returnedDonation);
  }


  @Test
  public void testCreateDonations () throws Exception
  {
    Donation donation1 = new Donation (123, "cash");
    Donation donation2 = new Donation (450, "cash");
    Donation donation3 = new Donation (43,  "paypal");

    Donation returnedDonation1 = DonationServiceAPI.createDonation(marge, donation1);
    Donation returnedDonation2 = DonationServiceAPI.createDonation(marge, donation2);
    Donation returnedDonation3 = DonationServiceAPI.createDonation(marge, donation3);

    assertEquals(donation1, returnedDonation1);
    assertEquals(donation2, returnedDonation2);
    assertEquals(donation3, returnedDonation3);

    DonationServiceAPI.deleteDonation(marge, returnedDonation1);
    DonationServiceAPI.deleteDonation(marge, returnedDonation2);    
    DonationServiceAPI.deleteDonation(marge, returnedDonation3);
  }

  @Test
  public void testListDonations () throws Exception
  {
    Donation donation1 = new Donation (123, "cash");
    Donation donation2 = new Donation (450, "cash");
    Donation donation3 = new Donation (43,  "paypal");

    DonationServiceAPI.createDonation(marge, donation1);
    DonationServiceAPI.createDonation(marge, donation2);
    DonationServiceAPI.createDonation(marge, donation3);

    List<Donation> donations = DonationServiceAPI.getDonations(marge);
    assertEquals (3, donations.size());

    assertTrue(donations.contains(donation1));
    assertTrue(donations.contains(donation2));
    assertTrue(donations.contains(donation3));

    DonationServiceAPI.deleteDonation(marge, donations.get(0));
    DonationServiceAPI.deleteDonation(marge, donations.get(1));    
    DonationServiceAPI.deleteDonation(marge, donations.get(2));
  }  
}

All tests should pass.

Exercises

This is an archive of the two projects so far:

Exercise 1

Deploy the play project to heroku. Verify that the tests work against the deployed version.

Exercise 2

Are there any other API features we could implement?

How about: