Complete the API + write a range of unit tests to exercises the features. Deploy the service to the cloud.
interface PacemakerInterface {
// ...
@GET("/users/{id}/activities/{activityId}")
Call<Activity> getActivity(@Path("id") String id, @Path("activityId") String activityId);
}
public Activity getActivity(String userId, String activityId) {
Activity activity = null;
try {
Call<Activity> call = pacemakerInterface.getActivity(userId, activityId);
Response<Activity> response = call.execute();
activity = response.body();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return activity;
}
@Command(description = "List all locations for a specific activity")
public void listActivityLocations(@Param(name = "activity-id") String id) {
Optional<Activity> activity = Optional.fromNullable(paceApi.getActivity(loggedInUser.getId(), id));
if (activity.isPresent()) {
// console.renderLocations(activity.get().route);
}
}
interface PacemakerInterface {
// ...
@POST("/users/{id}/activities/{activityId}/locations")
Call<Location> addLocation(@Path("id") String id, @Path("activityId") String activityId, @Body Location location);
}
public void addLocation(String id, String activityId, double latitude, double longitude) {
try {
Call<Location> call = pacemakerInterface.addLocation(id, activityId, new Location(latitude, longitude));
call.execute();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
Also, addLocation method on PacemakerConsoleService will need to be updateD:
@Command(description = "Add location: Append location to an activity")
public void addLocation(@Param(name = "activity-id") String id,
@Param(name = "longitude") double longitude, @Param(name = "latitude") double latitude) {
Optional<Activity> activity = Optional.fromNullable(paceApi.getActivity(loggedInUser.getId(), id));
if (activity.isPresent()) {
paceApi.addLocation(loggedInUser.getId(), activity.get().id, latitude, longitude);
console.println("ok");
} else {
console.println("not found");
}
}
Implement the remaining user endpoints:
GET /user/:id
DELETE /users
DELETE /users/:id
This will require additional work in two projects:
The Endpoints will need to be implementED in the server, and then exposed in the client.
We have new endpoint to make available:
app.delete("/users", ctx -> {
service.deleteUsers(ctx);
});
app.delete("/users/:id", ctx -> {
service.deletetUser(ctx);
});
public void deletetUser(Context ctx) {
String id = ctx.param("id");
ctx.json(pacemaker.deleteUser(id));
}
public void deleteUsers(Context ctx) {
pacemaker.deleteUsers();
ctx.json(204);
}
We need these methods to be implemented:
Additional methods in PacemakerInterface
@DELETE("/users")
Call<User> deleteUsers();
@DELETE("/users/{id}")
Call<User> deleteUser(@Path("id") String id);
@GET("/users/{id}")
Call<User> getUser(@Path("id") String id);
And these are the additional methods in PacemakerAPI
public User getUser(String id) {
User user = null;
try {
Call<User> call = pacemakerInterface.getUser(id);
Response<User> response = call.execute();
user = response.body();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return user;
}
public void deleteUsers() {
try {
Call<User> call = pacemakerInterface.deleteUsers();
call.execute();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public User deleteUser(String id) {
User user = null;
try {
Call<User> call = pacemakerInterface.deleteUser(id);
Response<User> response = call.execute();
user = response.body();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return user;
}
We will tests these endpoints in the following steps.
In pacemaker-skeleton-client, we can now proceed to implement some initial tests.
In the test folder, create two packages: models and controllers.
In the models package, we can start by bringing over some fixtures:
package models;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Fixtures {
public static List<User> users =
new ArrayList<>(Arrays.asList(new User("marge", "simpson", "marge@simpson.com", "secret"),
new User("lisa", "simpson", "lisa@simpson.com", "secret"),
new User("bart", "simpson", "bart@simpson.com", "secret"),
new User("maggie", "simpson", "maggie@simpson.com", "secret")));
public static List<Activity> activities = new ArrayList<>(
Arrays.asList(new Activity("walk", "fridge", 0.001),
new Activity("walk", "bar", 1.0),
new Activity("run", "work", 2.2),
new Activity("walk", "shop", 2.5),
new Activity("cycle", "school", 4.5)));
public static List<Location> locations = new ArrayList<>(Arrays.asList(new Location(23.3, 33.3),
new Location(34.4, 45.2), new Location(25.3, 34.3), new Location(44.4, 23.3)));
public static List<Activity> margeActivities =
new ArrayList<>(Arrays.asList(activities.get(0), activities.get(1)));
public static List<Activity> lisasActivities =
new ArrayList<>(Arrays.asList(activities.get(2), activities.get(3)));
public static List<Location> route1 =
new ArrayList<>(Arrays.asList(locations.get(0), locations.get(1)));
public static List<Location> route2 =
new ArrayList<>(Arrays.asList(locations.get(2), locations.get(3)));
public static List<Activity> activitiesSortedByType =
new ArrayList<>(Arrays.asList(activities.get(4), activities.get(2), activities.get(1),
activities.get(0), activities.get(3)));
}
There are more or less the same as the fixture we had in the sample solution to assignment 1.
We can now try this test:
package controllers;
import static org.junit.Assert.*;
import java.util.Collection;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import models.User;
import static models.Fixtures.users;
public class UserTest {
PacemakerAPI pacemaker = new PacemakerAPI("http://localhost:7000");
User homer = new User("homer", "simpson", "homer@simpson.com", "secret");
@Before
public void setup() {
pacemaker.deleteUsers();
}
@After
public void tearDown() {
}
@Test
public void testCreateUser() {
User user = pacemaker.createUser(homer.firstname, homer.lastname, homer.email, homer.password);
assertEquals(user, homer);
User user2 = pacemaker.getUserByEmail(homer.email);
assertEquals(user2, homer);
}
@Test
public void testCreateUsers() {
users.forEach(
user -> pacemaker.createUser(user.firstname, user.lastname, user.email, user.password));
Collection<User> returnedUsers = pacemaker.getUsers();
assertEquals(users.size(), returnedUsers.size());
}
}
Remember - you must first run pacemaker-skeleton before running these tests. If these tests fail, it could be because you have previously added users to your current app session via the RESTLET client. If this is the case, terminate the pacemaker-skeleton session, restart it and try the tests again. They should pass.
Notice that setup()
deletes all users before each test. Commenting this out will cause one of the tests to fail. Can you see why?
We can now compose this new unit test to exercise the activity endpoint:
package controllers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import models.Activity;
import models.User;
public class ActivityTest {
PacemakerAPI pacemaker = new PacemakerAPI("http://localhost:7000");
User homer = new User("homer", "simpson", "homer@simpson.com", "secret");
@Before
public void setup() {
pacemaker.deleteUsers();
homer = pacemaker.createUser(homer.firstname, homer.lastname, homer.email, homer.password);
}
@After
public void tearDown() {
}
@Test
public void testCreateActivity() {
Activity activity = new Activity("walk", "shop", 2.5);
Activity returnedActivity = pacemaker.createActivity(homer.id, activity.type, activity.location, activity.distance);
assertEquals(activity.type, returnedActivity.type);
assertEquals(activity.location, returnedActivity.location);
assertEquals(activity.distance, returnedActivity.distance, 0.001);
assertNotNull(returnedActivity.id);
}
}
The createActivity test above is structured as follows:
Make sure the server is running - and run the test.
Currently we might say that the activities
endpoint is not quite testable yet - until we implement some way of removing activities. We will implement an API to remove all activities for a given user, and then write some tests to exercise this new feature.
First - start with the server:
We need to implement new api - delete all activities for a user. This requires 3 changes:
Method to remove all activities for a given user from our model:
public void deleteActivities(String id) {
Optional<User> user = Optional.fromNullable(userIndex.get(id));
if (user.isPresent()) {
user.get().activities.values().forEach(activity -> activitiesIndex.remove(activity.getId()));
user.get().activities.clear();
}
}
Service implementation - which will be invoked by Javalin when this delete request is triggered:
public void deleteActivities(Context ctx) {
String id = ctx.param("id");
pacemaker.deleteActivities(id);
ctx.json(204);
}
The registration of the new api path with Javalin:
app.delete("/users/:id/activities", ctx -> {
service.deleteActivities(ctx);
});
Add the following route:
@DELETE("/users/{id}/activities")
Call<String> deleteActivities(@Path("id") String id);
and the asssociated method call:
public void deleteActivities(String id) {
try {
Call<String> call = pacemakerInterface.deleteActivities(id);
call.execute();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
We can now write some new tests:
@Test
public void testGetActivity() {
Activity activity = new Activity("run", "fridge", 0.5);
Activity returnedActivity1 = pacemaker.createActivity(homer.id, activity.type, activity.location, activity.distance);
Activity returnedActivity2 = pacemaker.getActivity(homer.id, returnedActivity1.id);
assertEquals(returnedActivity1, returnedActivity2);
}
@Test
public void testDeleteActivity() {
Activity activity = new Activity("sprint", "pub", 4.5);
Activity returnedActivity = pacemaker.createActivity(homer.id, activity.type, activity.location, activity.distance);
assertNotNull (returnedActivity);
pacemaker.deleteActivities(homer.id);
returnedActivity = pacemaker.getActivity(homer.id, returnedActivity.id);
assertNull (returnedActivity);
}
Read these tests carefully - and verify that they all pass.
If you find your testGetActivity fails, it could be that you don't have the route in your RestMain configRoutes. Try adding this:
app.get("/users/:id/activities/:activityId", ctx -> {
service.getActivity(ctx);
});
In order to deploy pacemaker-skeleton we need to make some small modifications to the the pacemaker-skeleton project
First, introduce a new method into the RestMain class:
private static int getAssignedPort() {
ProcessBuilder processBuilder = new ProcessBuilder();
if (processBuilder.environment().get("PORT") != null) {
return Integer.parseInt(processBuilder.environment().get("PORT"));
}
return 7000;
}
This is required as we need to detect the port the cloud provider may have assigned to our app on startup (7000 is only for local test/development).
The main method in RestMain can now be changed to this:
public static void main(String[] args) throws Exception {
Javalin app = Javalin.create();
app.port(getAssignedPort());
app.start();
PacemakerRestService service = new PacemakerRestService();
configRoutes(app, service);
}
The pom for pacemaker-skeleton will need to be augmented to support deployment.
First, rename the version as follows:
<groupId>pacemaker</groupId>
<artifactId>pacemaker-skeleton</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
We have just removed SNAPSHOT from the version information.
Now introduce a new set of plugins:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>controllers.RestMain</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>com.heroku.sdk</groupId>
<artifactId>heroku-maven-plugin</artifactId>
<version>1.1.3</version>
<configuration>
<jdkVersion>1.8</jdkVersion>
<appName>YOUR-APP-NAME</appName>
<processTypes>
<web>java -jar target/pacemaker-skeleton-1.0-jar-with-dependencies.jar</web>
</processTypes>
</configuration>
</plugin>
This must be in the <plugins>
section of the POM.
Note the <appName>YOUR-APP-NAME</appName>
element - you will replace this with your own application name in the next step.
Just to make sure that the pom file is without errors by entering the following commands:
mvn clean
mvn package
The 'package' command will pull down a few new dependencies from the remote repo, so make sure you are connected to the Internet.
Now make sure that the ./target
folder contains this file:
This is a compiled version of our application, containing all dependencies. This is the artifact that will be deployed to the cloud (in the next step).
We will use the Heroku provider. Create a free account on the service now:
You will also need to download and install the Heroku CLI:
Once installed, open a command prompt/shell inside the pacemaker-skeleton folder
, and log in to heroku:
heroku login
Email: youremail@domain.com
Password: **********
Logged in as youremail@domain.com
Once logged in - you have a range of commands available:
We will be interested in these commands:
Lets try to create an app:
heroku create
Creating app... done, ⬢ fierce-brook-96934
https://fierce-brook-96934.herokuapp.com/ | https://git.heroku.com/fierce-brook-96934.git
Heroku generates the url when you create the app. If you forget it, you can list all apps:
heroku apps
The apps will also be listed on the web app (if you are logged in)
Before we deploy the app - we need to place the application name (as generated above) into our POM
<appName>fierce-brook-96934</appName>
(This was YOUR-APP-NAME previously).
From within the application folder, enter this command to deploy:
mvn heroku:deploy
This will take a few minutes - and should conclude with something like this:
[INFO] -----> Packaging application...
[INFO] - app: fierce-brook-96934
[INFO] - including: target/
[INFO] -----> Creating build...
[INFO] - file: target/heroku/slug.tgz
[INFO] - size: 12MB
[INFO] -----> Uploading build...
[INFO] - success
[INFO] -----> Deploying...
[INFO] remote:
[INFO] remote: -----> heroku-maven-plugin app detected
[INFO] remote: -----> Installing OpenJDK 1.8... done
[INFO] remote: -----> Discovering process types
[INFO] remote: Procfile declares types -> web
[INFO] remote:
[INFO] remote: -----> Compressing...
[INFO] remote: Done: 61.1M
[INFO] remote: -----> Launching...
[INFO] remote: Released v7
[INFO] remote: https://fierce-brook-96934.herokuapp.com/ deployed to Heroku
[INFO] remote:
[INFO] -----> Done
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 54.253 s
[INFO] Finished at: 2017-11-23T12:28:47Z
[INFO] Final Memory: 36M/622M
[INFO] ------------------------------------------------------------------------
If all goes well, the application should be deployed on the generated url. The application does not have a UX, so you will need to test it via the api:
If you wish to see the application console - for debugging or other purposes - you can run this command :
heroku logs --tail
This will reveal the console, more or less as you would have seen it in Eclipse when running locally:
Project Archives at this stage:
Test the deployed api. Currently, when the tests in pacemaker-skeleton-client launches, it is expecting the API on localhost:
PacemakerAPI pacemaker = new PacemakerAPI("http://localhost:7000");
Change this to your deployed application url:
PacemakerAPI pacemaker = new PacemakerAPI("https://fierce-brook-34.herokuapp.com/");
(Use your own url here)
And run all tests. They should pass. Keep an eye on the logs as the test run.
Try the same experiment with the pacemaker CLI. I.e. change PacemakerConsoleService
public class PacemakerConsoleService {
private PacemakerAPI paceApi = new PacemakerAPI("http://localhost:7000");
to use the deployed application:
public class PacemakerConsoleService {
PacemakerAPI pacemaker = new PacemakerAPI("https://fierce-brook-34.herokuapp.com/");
Explore the deployed service using the Restlet Chrome plugin
Evolve a set of tests to verify that locations can be added to activities.