Extend the pacemaker-service application to incorporate service layer. Utilize the layer in the pacemaker-android to retrieve a list of current users
This is v3 of the pacemaker-android project from the last android lab:
You can import this for reference purposes, or keep working with your own version.
Currently the pacemaker-android app does not have an ability for a user to logout. Introduce the following changes to introduce this feature:
public class PacemakerApp extends Application
{
...
public void logout()
{
loggedInUser = null;
}
<string name="action_logout">Logout</string>
<string name="action_create_activities">Create Activity</string>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:showAsAction="never"
android:title="@string/action_settings"/>
<item
android:id="@+id/action_list_actvities"
android:orderInCategory="100"
android:showAsAction="never"
android:title="@string/action_list_activities"/>
<item
android:id="@+id/action_logout"
android:orderInCategory="100"
android:showAsAction="never"
android:title="@string/action_logout"/>
</menu>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:showAsAction="never"
android:title="@string/action_settings"/>
<item
android:id="@+id/action_create_actvities"
android:orderInCategory="100"
android:showAsAction="never"
android:title="@string/action_create_activities"/>
<item
android:id="@+id/action_logout"
android:orderInCategory="100"
android:showAsAction="never"
android:title="@string/action_logout"/>
</menu>
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_list_actvities : startActivity (new Intent(this, ActivitiesList.class));
break;
case R.id.action_logout : startActivity (new Intent(this, Welcome.class));
break;
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_create_actvities : startActivity (new Intent(this, CreateActivity.class));
break;
case R.id.action_logout : startActivity (new Intent(this, Welcome.class));
break;
}
return true;
}
Currently a single list of activities is maintained, regardless of which user is logged on. We can easily refactor this by altering the data structure as follows:
private Map<String, List<Activity>> activities = new HashMap<String, List<Activity>>();
public void registerUser(User user)
{
users.put(user.email, user);
activities.put(user.email, new ArrayList<Activity>());
}
public void createActivity (Activity activity)
{
if (loggedInUser != null)
{
List<Activity> usersActivities = activities.get(loggedInUser.email);
activities.put(loggedInUser.email, usersActivities);
usersActivities.add(activity);
}
}
public List<Activity> getActivities()
{
List<Activity> usersActivities = null;
if (loggedInUser != null)
{
usersActivities = activities.get(loggedInUser.email);
}
return usersActivities;
}
If we wish to build a service layer into pacemaker-service, then we need an agreed data format for exchanging objects. JSON is the accepted format for the style of interface we are proposing.
We need to extend the pacemaker-service project as follows:
This is the build configuration for the application, which extend to incorporate the json library we have selected:
name := "pacemakerplay"
version := "1.0-SNAPSHOT"
libraryDependencies ++= Seq(
javaJdbc,
javaEbean,
cache,
"net.sf.flexjson" % "flexjson" % "3.1"
)
play.Project.playJavaSettings
The project will need to be rebuilt, and the eclipise view regenerated:
play
compile
eclipse
The Android project will need to use the same library:
Copy:
to 'lib' folder and add to build path.
Both projects can use the same serializers. Place this class in 'the models' package in both projects:
import java.util.ArrayList;
import java.util.List;
import flexjson.JSONDeserializer;
import flexjson.JSONSerializer;
public class JsonParser
{
public static JSONSerializer userSerializer = new JSONSerializer().exclude("class")
.exclude("persistent")
.exclude("entityId");
public static JSONSerializer activitySerializer = new JSONSerializer().exclude("class")
.exclude("persistent")
.exclude("entityId");
public static User json2User(String json)
{
return new JSONDeserializer<User>().deserialize(json, User.class);
}
public static List<User> json2Users(String json)
{
return new JSONDeserializer<ArrayList<User>>().use("values", User.class)
.deserialize(json);
}
public static String user2Json(Object obj)
{
return userSerializer.serialize(obj);
}
public static Activity json2Activity(String json)
{
Activity activity = new JSONDeserializer<Activity>().deserialize(json, Activity.class);
return activity;
}
public static String activity2Json(Object obj)
{
return activitySerializer.serialize(obj);
}
public static List<Activity> json2Activities (String json)
{
return new JSONDeserializer<ArrayList<Activity>>().use("values", Activity.class).deserialize(json);
}
}
We now define the API proper in pacemaker-service"
# API
GET /api/users controllers.PacemakerAPI.users()
DELETE /api/users controllers.PacemakerAPI.deleteAllUsers()
POST /api/users controllers.PacemakerAPI.createUser()
GET /api/users/:id controllers.PacemakerAPI.user(id: Long)
DELETE /api/users/:id controllers.PacemakerAPI.deleteUser(id: Long)
PUT /api/users/:id controllers.PacemakerAPI.updateUser(id: Long)
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)
package controllers;
import static models.JsonParser.*;
import java.util.List;
import models.Activity;
import models.User;
import play.mvc.Controller;
import play.mvc.Result;
public class PacemakerAPI extends Controller
{
public static Result users()
{
List<User> users = User.findAll();
return ok(user2Json(users));
}
public static Result user(Long id)
{
User user = User.findById(id);
return user==null? notFound() : ok(user2Json(user));
}
public static Result createUser()
{
User user = json2User(request().body().asJson().toString());
user.save();
return ok(user2Json(user));
}
public static Result deleteUser(Long id)
{
Result result = notFound();
User user = User.findById(id);
if (user != null)
{
user.delete();
result = ok();
}
return result;
}
public static Result deleteAllUsers()
{
User.deleteAll();
return ok();
}
public static Result updateUser(Long id)
{
Result result = notFound();
User user = User.findById(id);
if (user != null)
{
User updatedUser = json2User(request().body().asJson().toString());
user.update(updatedUser);
user.save();
result = ok(user2Json(user));
}
return result;
}
public static Result activities (Long userId)
{
User p = User.findById(userId);
return ok(activity2Json(p.activities));
}
public static Result createActivity (Long userId)
{
User user = User.findById(userId);
Activity activity = json2Activity(request().body().asJson().toString());
user.activities.add(activity);
user.save();
return ok(activity2Json(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(activity2Json(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 = json2Activity(request().body().asJson().toString());
activity.distance = updatedActivity.distance;
activity.location = updatedActivity.location;
activity.kind = updatedActivity.kind;
activity.save();
return ok(activity2Json(updatedActivity));
}
else
{
return badRequest();
}
}
}
}
Make sure the app is running, and browse to :
Viewing the page source, you should see:
[]
Also, if using chrome, keep an eye on the actual http traffic (via Tools menu ->Developers Tools):
Try browsing to this url:
The browser will be blank, however, the developer tools should display the actual response:
This reveals a '404' not found error.
More comprehensive tools for testing http interfaces are available. Search for the Chrome 'Postman REST Client' - and install as a chrome app on your workstation. Using Postman, experiment with the two links we tested above:
Now try to replicate the request as shown below (note the `Headers' Content-Type set to 'application/json':
When you press 'Send' you may see this response:
No try this request again:
In the above interchange, we have use the REST interface to create a User, and then subsequently get the user (by the id 1).
Create a few more users and verify that you can get them. See if you can also do the following:
Also, examine the database via the h2-console, and see if you can verify if the 'my_users' table is being updated.
We need new package of classes in the android application to converse with the pacemaker-service app:
Create a new package called 'org.pacemaker.http', and incorporate these classes:
package org.pacemaker.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.setResponse((List)result);
}
else
{
responder.setResponse(result);
}
}
}
}
package org.pacemaker.http;
import java.util.List;
public interface Response<T>
{
public void setResponse(List<T> aList);
public void setResponse(T anObject);
public void errorOccurred (Exception e);
}
package org.pacemaker.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);
}
}
Create a new class in the 'org.pacemaker.main' package to encapsulate API access:
package org.pacemaker.main;
import java.util.List;
import org.pacemaker.http.Request;
import org.pacemaker.http.Response;
import org.pacemaker.http.Rest;
import org.pacemaker.models.JsonParser;
import org.pacemaker.models.User;
import android.content.Context;
public class PacemakerAPI
{
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);
}
}
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 = JsonParser.json2Users(response);
return userList;
}
}
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", JsonParser.user2Json(params[0]));
return JsonParser.json2User(response);
}
}
In order to use network access from android, you will need to specifically enable it in the android manifest:
<uses-permission android:name="android.permission.INTERNET"/>
We can now retrieve the users list from the welcome activity:
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
app = (PacemakerApp) getApplication();
PacemakerAPI.getUsers(this, app, "Retrieving list of users");
}
Note that the second paramater in the above - app
- must implement the Response interface:
public class PacemakerApp extends Application implements Response<User>
{
private Map<String, User> users = new HashMap<String, User>();
private Map<String, List<Activity>> activities = new HashMap<String, List<Activity>>();
private User loggedInUser;
private boolean connected = false;
@Override
public void setResponse(List<User> aList)
{
connected = true;
for (User user : aList)
{
users.put(user.email, user);
}
}
@Override
public void setResponse(User anObject)
{
connected = true;
}
@Override
public void errorOccurred(Exception e)
{
connected = false;
Toast toast = Toast.makeText(this, "Failed to connect to Pacemaker Service", Toast.LENGTH_SHORT);
toast.show();
}
Archive of the app so far:
Currently PacemakerApp.registerUser creates a local User object, and has no impact on the server. Change this such that, when registering a new user, the user object is created on the server.
PacemakerApp.getActivities and PacemakerApp.createActivities are still just working with the local copies. Rework these such that they are creating/retreiving activities to/from the pacemakerplay service.