Objectives

Play Donation App

Using Play, create new app in the usual way:

play new donation-service

Generate an eclipse view of the project"

play eclipsify

... and import the project into your Eclipse workspace.

Create a new package called 'models' - and bring in these classes:

package models;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import java.util.List;

import play.db.jpa.Model;

@Entity
public class Donation extends Model
{
  public int    amount;
  public String method;

  public Donation (int amount, String method)
  {
    this.amount = amount;
    this.method = method;
  }

  public String toString()
  {
    return amount + ", " + method;
  }
}
package models;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import java.util.List;

import play.db.jpa.Model;

@Entity
public class User extends Model
{
  public String firstName;
  public String lastName;
  public String email;
  public String password;

  public User(String firstName, String lastName, String email, String password)
  {
    this.firstName = firstName;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  } 
}

Utils

Create a new package called 'utils', and bring in these classes:

package utils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

import play.data.binding.Global;
import play.data.binding.TypeBinder;

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

@Global
public class GsonBinder implements TypeBinder<JsonElement>
{
  public Object bind(String name, Annotation[] notes, String value, Class toClass, Type toType) throws Exception
  {
    return new JsonParser().parse(value);
  }
}
package utils;

import java.lang.reflect.Type;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import models.Donation;
import models.User;

public class JsonParsers
{
  static Gson gson = new Gson();

  public static User json2User(String json)
  {
    return gson.fromJson(json, User.class);   
  }

  public static List<User> json2Users(String json)
  {
    Type collectionType = new TypeToken<List<User>>() {}.getType();
    return gson.fromJson(json, collectionType); 
  }

  public static String user2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static Donation json2Donation(String json)
  {
    return gson.fromJson(json, Donation.class);    
  }

  public static String donation2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static List<Donation> json2Donations(String json)
  {
    Type collectionType = new TypeToken<List<Donation>>() {}.getType();
    return gson.fromJson(json, collectionType);    
  }  
}

API

Into the controllers package, create a new controller called 'DonationServiceAPI':

package controllers;

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

import java.util.*;

import com.google.gson.JsonElement;

import models.*;

public class DonationServiceAPI 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");
  }    

  public static void donations()
  {
    List<Donation> donations = Donation.findAll();
    renderText(JsonParsers.donation2Json(donations));
  }

  public static void donation (Long id)
  {
   Donation donation = Donation.findById(id);
   renderJSON (JsonParsers.donation2Json(donation));
  }

  public static void createDonation(JsonElement body)
  {
    Donation donation = JsonParsers.json2Donation(body.toString());
    Donation newDonation = new Donation (donation.amount, donation.method);
    newDonation.save();
    renderJSON (JsonParsers.donation2Json(newDonation));
  }  


  public static void deleteDonation(Long id)
  {
    Donation donation = Donation.findById(id);
    if (donation == null)
    {
      notFound();
    }
    else
    {
      donation.delete();
      ok();
    }
  }  
}

Config

Enable our in-memory database by uncommenting the usual setting in 'application.conf'

db=mem

Introduce the 'Bootstrap' java class directly into the 'app' folder:

import java.util.List;

import play.jobs.*;
import play.test.*;
import models.*;

@OnApplicationStart
public class Bootstrap extends Job 
{ 
  public void doJob()
  {
    if (User.count() == 0)
    {
     Fixtures.deleteDatabase(); 
     Fixtures.loadModels("data.yml");
    }
  }
}

.. and now specify our routes in config/routes:

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

GET     /api/donations                       DonationServiceAPI.donations
GET     /api/donations/{id}                  DonationServiceAPI.donation
POST    /api/donations                       DonationServiceAPI.createDonation
DELETE  /api/donations/{id}                  DonationServiceAPI.deleteDonation

Finally, provide some initial database entries in conf/data.yaml:

User(homer):
    usaCitizen: true
    firstName: Homer
    lastName: Simpson
    email: homer@simpson.com
    password: secret

User(marge):
    usaCitizen: true
    firstName: Marge
    lastName: Simpson
    email: marge@simpson.com
    password: secret

User(lisa):
    usaCitizen: true
    firstName: Lisa
    lastName: Simpson
    email: lisa@simpson.com
    password: secret

User(bart):
    usaCitizen: true
    firstName: Bart
    lastName: Simpson
    email: bart@simpson.com
    password: secret

User(maggie):
    usaCitizen: true
    firstName: Maggie
    lastName: Simpson
    email: maggie@simpson.com
    password: secret

Donation(a):
    amount : 210
    method : paypal

Donation(b):
    amount : 20
    method : cash

Donation(c):
    amount : 330
    method : cash

Donation(d):
    amount : 10
    method : paypal 

The project should build without error. Launch it and browse to the database in the usual way.

Also, in a browser, explore these urls:

Back in donation-android App

There is a folder in your eclipse project called 'libs'. Download and copy this jar into libs:

In Eclipse, create new package app.http, and bring in these classes:

Response

package app.http;

import java.util.List;

public interface Response<T>
{
  public void setReponse(List<T> aList);
  public void setReponse(T anObject);
  public void errorOccurred (Exception e);
}

Request

package app.http;

import java.util.List;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;

@SuppressWarnings("rawtypes")
public abstract class Request extends AsyncTask<Object, Void, Object>
{
  public Response         responder;
  public ProgressDialog   dialog;
  public Context          context;
  public String           message;
  public Exception        error;

  public Request(Context context, Response responder, String message)
  {
    this.responder = responder;
    this.context = context;
    this.message = message;
  }

  @Override
  protected void onPreExecute()
  {
    super.onPreExecute();
    this.dialog = new ProgressDialog(context, 1);
    this.dialog.setMessage(message);
    this.dialog.show();
  }

  @Override
  protected  Object doInBackground(Object... params)
  {
    error = null;
    try
    {
      return doRequest(params);
    }
    catch (Exception e)
    {
      error = e;
    }
    return null;
  }

  protected abstract  Object doRequest(Object... params) throws Exception;

  @SuppressWarnings("unchecked")
  @Override
  protected void onPostExecute(Object result)
  {
    super.onPostExecute(result);
    if (dialog.isShowing())
    {
      dialog.dismiss();
    }
    if (error != null)
    {
      responder.errorOccurred(error);  
    }
    else
    {
      if (result instanceof List)
      {
        responder.setReponse((List)result);
      }
      else
      {
        responder.setReponse(result);
      }
    }
  } 
}

Rest

package app.http;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

public class Rest
{
  private static DefaultHttpClient httpClient = null;
  private static final String URL = "http://10.0.2.2:9000";

  private static DefaultHttpClient httpClient()
  {
    if (httpClient == null)
    {
      HttpParams httpParameters = new BasicHttpParams();
      HttpConnectionParams.setConnectionTimeout(httpParameters, 10000);
      HttpConnectionParams.setSoTimeout(httpParameters, 10000);
      httpClient = new DefaultHttpClient(httpParameters);
    }
    return httpClient;
  }

  public static String get(String path) throws Exception
  {
    HttpGet getRequest = new HttpGet(URL + path);
    getRequest.setHeader("accept", "application/json");
    HttpResponse response = httpClient().execute(getRequest);
    return new BasicResponseHandler().handleResponse(response);
  }

  public static String delete(String path) throws Exception
  {
    HttpDelete deleteRequest = new HttpDelete(URL + path);
    HttpResponse response = httpClient().execute(deleteRequest);
    return new BasicResponseHandler().handleResponse(response);
  }

  public static String put(String path, String json) throws Exception
  {
    HttpPut putRequest = new HttpPut(URL + path);
    putRequest.setHeader("Content-type", "application/json");
    putRequest.setHeader("accept", "application/json");

    StringEntity s = new StringEntity(json);
    s.setContentEncoding("UTF-8");
    s.setContentType("application/json");
    putRequest.setEntity(s);

    HttpResponse response = httpClient().execute(putRequest);
    return new BasicResponseHandler().handleResponse(response);
  }

  public static String post(String path, String json) throws Exception
  {
    HttpPost putRequest = new HttpPost(URL + path);
    putRequest.setHeader("Content-type", "application/json");
    putRequest.setHeader("accept", "application/json");

    StringEntity s = new StringEntity(json);
    s.setContentEncoding("UTF-8");
    s.setContentType("application/json");
    putRequest.setEntity(s);

    HttpResponse response = httpClient().execute(putRequest);
    return new BasicResponseHandler().handleResponse(response);
  }
}

Models

We already have a models package - introduce these new classes into this package:

JsonParsers

package app.models;

import java.lang.reflect.Type;
import java.util.List;


import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class JsonParsers
{
  static Gson gson = new Gson();

  public static User json2User(String json)
  {
    return gson.fromJson(json, User.class);   
  }

  public static List<User> json2Users(String json)
  {
    Type collectionType = new TypeToken<List<User>>() {}.getType();
    return gson.fromJson(json, collectionType); 
  }

  public static String user2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static Donation json2Donation(String json)
  {
    return gson.fromJson(json, Donation.class);    
  }

  public static String donation2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static List<Donation> json2Donations(String json)
  {
    Type collectionType = new TypeToken<List<Donation>>() {}.getType();
    return gson.fromJson(json, collectionType);    
  }  
}

DonationServiceAPI

package app.models;

import java.util.List;
import android.content.Context;
import app.http.Request;
import app.http.Response;
import app.http.Rest;

public class DonationServiceAPI
{ 
  public static void getUsers(Context context, Response<User> response, String dialogMesssage)
  {
    new GetUsers(context, response, dialogMesssage).execute();
  }

  public static void createUser(Context context, Response<User> response, String dialogMesssage, User user)
  {
    new CreateUser(context, response, dialogMesssage).execute(user);
  }

  public static void getDonations(Context context, Response<Donation> response, String dialogMesssage)
  {
    new GetDonations(context, response, dialogMesssage).execute();
  }

  public static void createDonation(Context context, Response<Donation> response, String dialogMesssage, Donation donation)
  {
    new CreateDonation(context, response, dialogMesssage).execute(donation);
  }
}

class GetUsers extends Request
{
  public GetUsers(Context context, Response<User> callback, String message)
  {
    super(context, callback, message);
  }

  @Override
  protected List<User> doRequest(Object... params) throws Exception
  {
    String response =  Rest.get("/api/users");
    List<User> userList = JsonParsers.json2Users(response);
    return userList;
  }
}

class GetDonations extends Request
{
  public GetDonations(Context context, Response<Donation> callback, String message)
  {
    super(context, callback, message);
  }

  @Override
  protected List<Donation> doRequest(Object... params) throws Exception
  {
    String response =  Rest.get("/api/donations");
    List<Donation> donationList = JsonParsers.json2Donations(response);
    return donationList;
  }
}

class CreateUser extends Request
{ 
  public CreateUser(Context context, Response<User> callback, String message)
  {
    super(context, callback, message);
  }

  @Override
  protected User doRequest(Object... params) throws Exception
  {
    String response = Rest.post ("/api/users", JsonParsers.user2Json(params[0]));
    return JsonParsers.json2User(response);
  }
}

class CreateDonation extends Request
{
  public CreateDonation(Context context, Response<Donation> callback, String message)
  {
    super(context, callback, message);
  }

  @Override
  protected Donation doRequest(Object... params) throws Exception
  {
    String response = Rest.post ("/api/donations", JsonParsers.donation2Json(params[0]));
    return JsonParsers.json2Donation(response);
  }
}

Login Activity

This is a restructuring of Login to fetch the users from the donation-service as soon as the activity starts, and then to handle the response appropriately.

package app.activities;

import java.util.List;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import app.donation.R;
import app.http.Response;
import app.main.DonationApp;
import app.models.DonationServiceAPI;
import app.models.User;

public class Login extends Activity implements Response<User>
{
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);

    DonationServiceAPI.getUsers(this, this, "Retrieving list of users");
  }

  public void signinPressed (View view) 
  {
    DonationApp app = (DonationApp) getApplication();

    TextView email     = (TextView)  findViewById(R.id.loginEmail);
    TextView password  = (TextView)  findViewById(R.id.loginPassword);

    if (app.validUser(email.getText().toString(), password.getText().toString()))
    {
      startActivity (new Intent(this, Donate.class));
    }
    else
    {
      Toast toast = Toast.makeText(this, "Invalid Credentials", Toast.LENGTH_SHORT);
      toast.show();
    }
  }

  @Override
  public void setReponse(List<User> aList)
  {
    DonationApp app = (DonationApp) getApplication();
    app.users = aList;
  }

  @Override
  public void errorOccurred(Exception e)
  {
    Toast toast = Toast.makeText(this, "Donation Service Unavailable. Try again later", Toast.LENGTH_LONG);
    toast.show();
    startActivity (new Intent(this, Welcome.class));
  }

  @Override
  public void setReponse(User anObject)
  {}
}

Running the application

The last step to getting the project running is to specifically enable Internet access for your app. This is controlled by a single entry in the AndroidManifest.xml file:

    <uses-permission android:name="android.permission.INTERNET"/>

Try the app now (make sure donation-service play app is running). It should let you log in with user credentials download from the service (try homer@simpson.com, secret - without registering).

Report

Finally, the report activity:

package app.activities;

import java.util.List;
import app.donation.R;
import app.http.Response;
import app.main.DonationApp;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import app.models.Donation;
import app.models.DonationServiceAPI;

public class Report extends Activity implements Response <Donation>
{
  private ListView        listView;
  private DonationApp     app;
  private DonationAdapter adapter; 

  @Override
  public boolean onCreateOptionsMenu(Menu menu)
  {
    getMenuInflater().inflate(R.menu.report, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case R.id.menuDonate : startActivity (new Intent(this, Donate.class));
                             break;
      case R.id.menuLogout : startActivity (new Intent(this, Welcome.class));
                             break;                               
    }
    return true;
  }  

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_report);

    app = (DonationApp) getApplication();

    listView = (ListView) findViewById(R.id.reportList);
    adapter = new DonationAdapter (this, app.donations);

    listView.setAdapter(adapter);

    DonationServiceAPI.getDonations(this, this, "Downloading Donations List..");
  }

  @Override
  public void setReponse(List<Donation> aList)
  {
    app.donations     = aList;
    adapter.donations = aList;
    adapter.notifyDataSetChanged();
  }

  @Override
  public void setReponse(Donation anObject)
  {
  }

  @Override
  public void errorOccurred(Exception e)
  {
    Toast toast = Toast.makeText(this, "Donation Service Unavailable!", Toast.LENGTH_LONG);
    toast.show();
    startActivity (new Intent(this, Welcome.class));
  }
}

class DonationAdapter extends ArrayAdapter<Donation>
{
  private Context        context;
  public  List<Donation> donations;

  public DonationAdapter(Context context, List<Donation> donations)
  {
    super(context, R.layout.row_donate, donations);
    this.context   = context;
    this.donations = donations;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent)
  {
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    View     view       = inflater.inflate(R.layout.row_donate, parent, false);
    Donation donation   = donations.get(position);
    TextView amountView = (TextView) view.findViewById(R.id.row_amount);
    TextView methodView = (TextView) view.findViewById(R.id.row_method);

    amountView.setText("" + donation.amount);
    methodView.setText(donation.method);

    return view;
  }

  @Override
  public int getCount()
  {
    return donations.size();
  }
}

When this starts - it requests the donations from the service. When they arrive, they are displayed in the list view.

Exercises

Archive of projects so far:

Exercise 1: Cloudbees

Publish the donation-service app to Cloudbees. Using the url of the cloudbees app, change the Rest URL:

public class Rest
{
  private static DefaultHttpClient httpClient = null;
  private static final String URL = "http://10.0.2.2:9000";
  //...

... to your cloudbees app. See if your android app now reads from the cloudbees app.

Exercise 2:

The application as it currently stands uses two aspects of the donation play application:

See if you can figure out how to:

The facilities are already in the play app for this, so you will only be making changes to the android application.

Because the donation-service play app has no UI, keep an eye on the database via the database browser in the usual way:

Exercise 3:

Migrate over the UI from the donation play app you completed during the summer school. This is a copy of the app here: