Introduce a new project, an implementation of the service in Kotlin.
Project Archives at this stage:
Evolve a set of tests to verify that locations can be added to activities.
In pacemaker-skeleton-client, we need an additional method in the PacemakerInterface:
@GET("/users/{id}/activities/{activityId}/locations")
Call<List<Location>> getLocations(@Path("id") String id, @Path("activityId") String activityId);
... and this matching method in PacemakerAPI:
public List<Location> getLocations(String id, String activityId) {
List<Location> locations = null;
try {
Call<List<Location>> call = pacemakerInterface.getLocations(id, activityId);
Response<List<Location>> response = call.execute();
locations = response.body();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return locations;
}
With these methods in place, this new test in ActivityTest should pass:
@Test
public void testCreateActivityWithSingleLocation() {
pacemaker.deleteActivities(homer.id);
Activity activity = new Activity("walk", "shop", 2.5);
Location location = new Location(12.0, 33.0);
Activity returnedActivity = pacemaker.createActivity(homer.id, activity.type, activity.location, activity.distance);
pacemaker.addLocation(homer.id, returnedActivity.id, location.latitude, location.longitude);
List<Location> locations = pacemaker.getLocations(homer.id, returnedActivity.id);
assertEquals (locations.size(), 1);
assertEquals (locations.get(0), location);
}
Verify that this test runs successfully.
This test should also run:
@Test
public void testCreateActivityWithMultipleLocation() {
pacemaker.deleteActivities(homer.id);
Activity activity = new Activity("walk", "shop", 2.5);
Activity returnedActivity = pacemaker.createActivity(homer.id, activity.type, activity.location, activity.distance);
locations.forEach (location -> pacemaker.addLocation(homer.id, returnedActivity.id, location.latitude, location.longitude));
List<Location> returnedLocations = pacemaker.getLocations(homer.id, returnedActivity.id);
assertEquals (locations.size(), returnedLocations.size());
assertEquals(locations, returnedLocations);
}
These imports are required:
import models.Location;
import static models.Fixtures.locations;
import java.util.List;
Notice that we are no longer using the id field in the Location class. We should remove it now as it is not neccessary in our current implementation. Be sure to remove in both client and server projects.
To ensure your refactoring of the Location classes hasn't disrupted anything, rerun your tests.
In eclipse, visit the Eclipse Marketplace (Help->Eclipse Marketplace), locate and install the Kotlin plugin:
You may need to restart eclipse.
Outside of Eclipse, create a new folder called pacemaker-skeleton-kotlin
, and create this new pom.xml
file:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pacemaker</groupId>
<artifactId>pacemaker-skeleton-kotlin</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>pacemaker-skeleton</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<kotlin.version>1.1.60</kotlin.version>
</properties>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<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.RestMainKt</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-kotlin-1.0-jar-with-dependencies.jar</web>
</processTypes>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<dependency>
<groupId>asg-cliche</groupId>
<artifactId>asg-cliche</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>java-ascii-table</groupId>
<artifactId>java-ascii-table</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>0.5.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.1</version>
</dependency>
</dependencies>
</project>
In Eclipse, import the folder as a Maven project.
Notice the project does not have a source folder yet. Create this folder structure now (on the command line or using Explorer/Finder)
└── src
├── main
└──kotlin
Back in Eclipse, refresh the project - and the (empty) source folder should appear:
Note: when coding in Kotlin, you might find it easier to switch to the Kotlin perspective in Eclipse (see the buttons on the top right hand corner of Eclipse).
Create a new package in the src/main/kotlin folder called models
, and create three new Kotlin classes:
These are the sources for these classes:
package models
data class Location(
val latitude: Double,
val longitude: Double)
package models
import java.util.UUID
data class Activity(
var type: String = "",
var location: String = "",
var distance: Float = 0.0f,
var id: String = UUID.randomUUID().toString(),
val route: MutableList<Location> = ArrayList())
package models
import java.util.UUID
data class User(
val firstname: String = "",
val lastname: String = "",
val email: String = "",
val password: String = "",
var id: String = UUID.randomUUID().toString(),
val activities: MutableMap<String, Activity> = hashMapOf<String, Activity>())
These are Kotlin Data Classes - they are functionally equivalent to the Java equivalents from pacemaker-skeleton, with much of the extra methods auto-generated.
Create three Kotlin classes in a new controllers package:
These are the sources:
package controllers
import java.util.UUID;
import models.Activity
import models.Location
import models.User
class PacemakerAPI {
var userIndex = hashMapOf<String, User>()
var emailIndex = hashMapOf<String, User>()
var activitiesIndex = hashMapOf<String, Activity>()
var users = userIndex.values
fun createUser(firstName: String, lastName: String, email: String, password: String): User {
var user = User(firstName, lastName, email, password)
userIndex[user.id] = user
emailIndex[user.email] = user
return user
}
fun deleteUsers() {
userIndex.clear();
emailIndex.clear()
}
fun getUser(id: String) = userIndex[id]
fun getUserByEmail(email: String) = emailIndex[email]
}
package controllers
import io.javalin.Context
import models.Activity
import models.Location
import models.User
class PacemakerRestService {
val pacemaker = PacemakerAPI()
fun listUsers(ctx: Context) {
ctx.json(pacemaker.users)
}
fun createUser(ctx: Context) {
val user = ctx.bodyAsClass(User::class.java)
val newUser = pacemaker.createUser(user.firstname, user.lastname, user.email, user.password)
ctx.json(newUser)
}
fun deleteUsers(ctx: Context) {
pacemaker.deleteUsers()
ctx.status(200)
}
}
package controllers
import io.javalin.Javalin
fun main(args: Array<String>) {
val app = Javalin.create().port(getHerokuAssignedPort()).start()
val service = PacemakerRestService()
configRoutes(app, service)
}
private fun getHerokuAssignedPort(): Int {
val processBuilder = ProcessBuilder()
return if (processBuilder.environment()["PORT"] != null) {
Integer.parseInt(processBuilder.environment()["PORT"])
} else 7000
}
fun configRoutes(app: Javalin, service: PacemakerRestService) {
app.get("/users") { ctx -> service.listUsers(ctx) }
app.post("/users") { ctx -> service.createUser(ctx) }
app.delete("/users") { ctx -> service.deleteUsers(ctx) }
}
You should be able to run the application now:
This is supporting the users endpoint - making the service available on http://localhost:7000
We already have a set of tests built to exercises these routes - in UsersTest
in pacemaker-skeleton-client.
Run these test now - they should pass:
The service is implemented in Kotlin - the tests are in Java.
Initial activity endpoints can now be implemented:
fun createActivity(id: String, type: String, location: String, distance: Float): Activity? {
var activity:Activity? = null
var user = userIndex.get(id)
if (user != null) {
activity = Activity(type, location, distance)
user.activities[activity.id] = activity
activitiesIndex[activity.id] = activity;
}
return activity;
}
fun getActivity(id: String): Activity? {
return activitiesIndex[id]
}
fun deleteActivities(id: String) {
require(userIndex[id] != null)
var user = userIndex.get(id)
if (user != null) {
for ((u, activity) in user.activities) {
activitiesIndex.remove(activity.id)
}
user.activities.clear();
}
}
fun getActivity(ctx: Context) {
// val userId: String? = ctx.param("id")
val activityId: String? = ctx.param("activityId")
val activity = pacemaker.getActivity(activityId!!)
if (activity != null) {
ctx.json(activity)
} else {
ctx.status(404)
}
}
fun getActivities(ctx: Context) {
val id: String? = ctx.param("id")
val user = pacemaker.getUser(id!!)
if (user != null) {
ctx.json(user.activities)
} else {
ctx.status(404)
}
}
fun createActivity(ctx: Context) {
val id: String? = ctx.param("id")
val user = pacemaker.getUser(id!!)
if (user != null) {
val activity = ctx.bodyAsClass(Activity::class.java)
val newActivity = pacemaker.createActivity(user.id, activity.type, activity.location, activity.distance)
ctx.json(newActivity!!)
} else {
ctx.status(404)
}
}
fun deleteActivites(ctx: Context) {
val id: String? = ctx.param("id")
pacemaker.deleteActivities(id!!);
ctx.status(204)
}
app.get("/users/:id/activities") { ctx -> service.getActivities(ctx) }
app.get("/users/:id/activities/:activityId") { ctx -> service.getActivity(ctx) }
app.post("/users/:id/activities") { ctx -> service.createActivity(ctx) }
app.delete("/users/:id/activities") { ctx -> service.deleteActivites(ctx) }
We can now try the ActivityTest from pacemaker-skeleton-client:
Notice that the location endpoints are failing - as we have not yet implemented them.
Note: if more of your tests have errors in them when compared with the above graphic, you might need to stop the Kotlin Rest service and restart it (to pick up the above changes).
Archive of this lab so far:
Archive of ALL labs (from lab 08 to lab 12) :
See accompanying video tour of the labs in this archive.
When submitting your final project, please download and complete the above pptx form outlining your approach and achievements in the project.
Implement the location endpoints so that the ActivityTest all pass
Deploy the application to Heroku. The same steps we have already carried out (in lab 11) can also be applied here.