Objectives

Develop a baseline for Assignment 2, to include a simplified version of pacemaker application developed so far

Setup

Create a new folder to contain the project. To might call it 'pacemaker-skeleton'

Directory Structure

In this folder, create the following directory structure:

pacemaker-skeleton
     │
     └── src
         │
         ├── main
         │   │ 
         │   └──java
         │
         └── test
             │
             └──java

pom.xml

In the root of the folder, create the 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</artifactId>
  <version>1.0-SNAPSHOT</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>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </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>
  </dependencies>
</project>

if you are using git, you might wish to use this .gitignore:

.idea
target
*.iml
.settings
.classpath
.project

We can now bring this project into Eclipse. Select File-Import, and locate Maven->Existing Maven Project:

The project should look like this:

Models

In Eclipse, create following package in the main/java source folder:

  • models

Here are revised and simplified models for this package:

Location

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.UUID;

import com.google.common.base.Objects;

public class Location implements Serializable {

  public String id;
  public double longitude;
  public double latitude;

  public Location() {
  }

  public String getId() {
    return id;
  }

  public double getLongitude() {
    return longitude;
  }

  public double getLatitude() {
    return latitude;
  }

  public Location(double latitude, double longitude) {
    this.id = UUID.randomUUID().toString();
    this.latitude = latitude;
    this.longitude = longitude;
  }


  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Location) {
      final Location other = (Location) obj;
      return Objects.equal(latitude, other.latitude)
          && Objects.equal(longitude, other.longitude);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(latitude)
        .addValue(longitude)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.latitude, this.longitude);
  }
}

Activity

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import com.google.common.base.Objects;

public class Activity implements Serializable {

  public String id;
  public String type;
  public String location;
  public double distance;

  public List<Location> route = new ArrayList<>();

  public Activity() {
  }

  public Activity(String type, String location, double distance) {
    this.id = UUID.randomUUID().toString();
    this.type = type;
    this.location = location;
    this.distance = distance;
  }

  public String getId() {
    return id;
  }

  public String getType() {
    return type;
  }

  public String getLocation() {
    return location;
  }

  public String getDistance() {
    return Double.toString(distance);
  }

  public String getRoute() {
    return route.toString();
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Activity) {
      final Activity other = (Activity) obj;
      return Objects.equal(type, other.type)
          && Objects.equal(location, other.location)
          && Objects.equal(distance, other.distance)
          && Objects.equal(route, other.route);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(type)
        .addValue(location)
        .addValue(distance)
        .addValue(route)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.type, this.location, this.distance);
  }
}

User

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import com.google.common.base.Objects;

public class User implements Serializable {

  public String id;
  public String firstName;
  public String lastName;
  public String email;
  public String password;

  public Map<String, Activity> activities = new HashMap<>();

  public User() {
  }

  public String getId() {
    return id;
  }

  public String getFirstname() {
    return firstName;
  }

  public String getLastname() {
    return lastName;
  }

  public String getEmail() {
    return email;
  }

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

  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof User) {
      final User other = (User) obj;
      return Objects.equal(firstName, other.firstName)
          && Objects.equal(lastName, other.lastName)
          && Objects.equal(email, other.email)
          && Objects.equal(password, other.password)
          && Objects.equal(activities, other.activities);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(firstName)
        .addValue(lastName)
        .addValue(password)
        .addValue(email)
        .addValue(activities)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.lastName, this.firstName, this.email, this.password);
  }
}

Parsers

In Eclipse, create following package in the main/java source folder:

  • parsers

Here are class for this package:

Parser

package parsers;

import java.util.Collection;
import java.util.List;
import models.Activity;
import models.Location;
import models.User;

public class Parser {

  public void println(String s) {
    System.out.println(s);
  }

  public void renderUser(User user) {
    System.out.println(user.toString());
  }

  public void renderUsers(Collection<User> users) {
    System.out.println(users.toString());
  }

  public void renderActivity(Activity activities) {
    System.out.println(activities.toString());
  }

  public void renderActivities(Collection<Activity> activities) {
    System.out.println(activities.toString());
  }

  public void renderLocations(List<Location> locations) {
    System.out.println(locations.toString());
  }
}

ASCIITableParser

package parsers;

import com.bethecoder.ascii_table.ASCIITable;
import com.bethecoder.ascii_table.impl.CollectionASCIITableAware;
import com.bethecoder.ascii_table.spec.IASCIITableAware;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import models.Activity;
import models.Location;
import models.User;

public class AsciiTableParser extends Parser {

  public void renderUser(User user) {
    if (user != null) {
      renderUsers(Arrays.asList(user));
      System.out.println("ok");
    } else {
      System.out.println("not found");
    }
  }

  public void renderUsers(Collection<User> users) {
    if (users != null) {
      if (!users.isEmpty()) {
        List<User> userList = new ArrayList<User>(users);
        IASCIITableAware asciiTableAware = new CollectionASCIITableAware<User>(userList, "id",
            "firstname",
            "lastname", "email");
        System.out.println(ASCIITable.getInstance().getTable(asciiTableAware));
      }
      System.out.println("ok");
    } else {
      System.out.println("not found");
    }
  }

  public void renderActivity(Activity activity) {
    if (activity != null) {
      renderActivities(Arrays.asList(activity));
      System.out.println("ok");
    } else {
      System.out.println("not found");
    }
  }

  public void renderActivities(Collection<Activity> activities) {
    if (activities != null) {
      if (!activities.isEmpty()) {
        List<Activity> activityList = new ArrayList(activities);
        IASCIITableAware asciiTableAware = new CollectionASCIITableAware<Activity>(activityList,
            "id",
            "type", "location", "distance", "starttime", "duration");
        System.out.println(ASCIITable.getInstance().getTable(asciiTableAware));
      }
      System.out.println("ok");
    } else {
      System.out.println("not found");
    }
  }

  public void renderLocations(List<Location> locations) {
    if (locations != null) {
      if (!locations.isEmpty()) {
        IASCIITableAware asciiTableAware = new CollectionASCIITableAware<Location>(locations,
            "id",
            "latitude", "longitude");
        System.out.println(ASCIITable.getInstance().getTable(asciiTableAware));
      }
      System.out.println("ok");
    } else {
      System.out.println("not found");
    }
  }
}

Controllers

In Eclipse, create following package in the main/java source folder:

  • controllers

Here are revised and simplified models for this package:

PacemakerAPI

package controllers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.common.base.Optional;

import models.Activity;
import models.Location;
import models.User;


public class PacemakerAPI {

  private Map<String, User> emailIndex = new HashMap<>();
  private Map<String, User> userIndex = new HashMap<>();
  private Map<String, Activity> activitiesIndex = new HashMap<>();

  public PacemakerAPI() {
  }

  public Collection<User> getUsers() {
    return userIndex.values();
  }

  public void deleteUsers() {
    userIndex.clear();
    emailIndex.clear();
  }

  public User createUser(String firstName, String lastName, String email, String password) {
    User user = new User(firstName, lastName, email, password);
    emailIndex.put(email, user);
    userIndex.put(user.id, user);
    return user;
  }

  public Activity createActivity(String id, String type, String location, double distance) {
    Activity activity = null;
    Optional<User> user = Optional.fromNullable(userIndex.get(id));
    if (user.isPresent()) {
      activity = new Activity(type, location, distance);
      user.get().activities.put(activity.id, activity);
      activitiesIndex.put(activity.id, activity);
    }
    return activity;
  }

  public Activity getActivity(String id) {
    return activitiesIndex.get(id);
  }

  public Collection<Activity> getActivities(String id) {
    Collection<Activity> activities = null;
    Optional<User> user = Optional.fromNullable(userIndex.get(id));
    if (user.isPresent()) {
      activities = user.get().activities.values();
    }
    return activities;
  }

  public List<Activity> listActivities(String userId, String sortBy) {
    List<Activity> activities = new ArrayList<>();
    activities.addAll(userIndex.get(userId).activities.values());
    switch (sortBy) {
      case "type":
        activities.sort((a1, a2) -> a1.type.compareTo(a2.type));
        break;
      case "location":
        activities.sort((a1, a2) -> a1.location.compareTo(a2.location));
        break;
      case "distance":
        activities.sort((a1, a2) -> Double.compare(a1.distance, a2.distance));
        break;
    }
    return activities;
  }

  public void addLocation(String id, double latitude, double longitude) {
    Optional<Activity> activity = Optional.fromNullable(activitiesIndex.get(id));
    if (activity.isPresent()) {
      activity.get().route.add(new Location(latitude, longitude));
    }
  }

  public User getUserByEmail(String email) {
    return emailIndex.get(email);
  }

  public User getUser(String id) {
    return userIndex.get(id);
  }

  public User deleteUser(String id) {
    User user = userIndex.remove(id);
    return emailIndex.remove(user.email);
  }
}

PacemakerConsoleService

package controllers;

import com.google.common.base.Optional;

import asg.cliche.Command;
import asg.cliche.Param;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import models.Activity;
import models.User;
import parsers.AsciiTableParser;
import parsers.Parser;

public class PacemakerConsoleService {

  private PacemakerAPI paceApi = new PacemakerAPI();;
  private Parser console = new AsciiTableParser();
  private User loggedInUser = null;

  public PacemakerConsoleService() {
  }

  // Starter Commands

  @Command(description = "Register: Create an account for a new user")
  public void register(@Param(name = "first name") String firstName,
      @Param(name = "last name") String lastName,
      @Param(name = "email") String email, @Param(name = "password") String password) {
  }

  @Command(description = "List Users: List all users emails, first and last names")
  public void listUsers() {
  }

  @Command(description = "Login: Log in a registered user in to pacemaker")
  public void login(@Param(name = "email") String email,
      @Param(name = "password") String password) {
  }

  @Command(description = "Logout: Logout current user")
  public void logout() {
  }

  @Command(description = "Add activity: create and add an activity for the logged in user")
  public void addActivity(
      @Param(name = "type") String type,
      @Param(name = "location") String location,
      @Param(name = "distance") double distance) {
  }

  @Command(description = "List Activities: List all activities for logged in user")
  public void listActivities() {
  }

  // Baseline Commands

  @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) {
  }

  @Command(description = "ActivityReport: List all activities for logged in user, sorted alphabetically by type")
  public void activityReport() {
  }

  @Command(description = "Activity Report: List all activities for logged in user by type. Sorted longest to shortest distance")
  public void activityReport(@Param(name = "byType: type") String sortBy) {
  }

  @Command(description = "List all locations for a specific activity")
  public void listActivityLocations(@Param(name = "activity-id") String id) {
  }

  @Command(description = "Follow Friend: Follow a specific friend")
  public void follow(@Param(name = "email") String email) {
  }

  @Command(description = "List Friends: List all of the friends of the logged in user")
  public void listFriends() {
  }

  @Command(description = "Friend Activity Report: List all activities of specific friend, sorted alphabetically by type)")
  public void friendActivityReport(@Param(name = "email") String email) {
  }

  // Good Commands

  @Command(description = "Unfollow Friends: Stop following a friend")
  public void unfollowFriend() {
  }

  @Command(description = "Message Friend: send a message to a friend")
  public void messageFriend(@Param(name = "email") String email,
      @Param(name = "message") String message) {
  }

  @Command(description = "List Messages: List all messages for the logged in user")
  public void listMessages() {
  }

  @Command(description = "Distance Leader Board: list summary distances of all friends, sorted longest to shortest")
  public void distanceLeaderBoard() {
  }

  // Excellent Commands

  @Command(description = "Distance Leader Board: distance leader board refined by type")
  public void distanceLeaderBoardByType(@Param(name = "byType: type") String type) {
  }

  @Command(description = "Message All Friends: send a message to all friends")
  public void messageAllFriends(@Param(name = "message") String message) {
  }

  @Command(description = "Location Leader Board: list sorted summary distances of all friends in named location")
  public void locationLeaderBoard(@Param(name = "location") String message) {
  }

  // Outstanding Commands

  // Todo
}

Main

package controllers;

import asg.cliche.Shell;
import asg.cliche.ShellFactory;

public class Main {

  public static void main(String[] args) throws Exception {
    PacemakerConsoleService main = new PacemakerConsoleService();
    Shell shell = ShellFactory
        .createConsoleShell("pm", "Welcome to pacemaker-console - ?help for instructions", main);
    shell.commandLoop();
  }
}

Run the application

If you run Main - and list all commands, you should see this report in the console:

Welcome to pacemaker-console - ?help for instructions
pm> ?la
abbrev  name  params
...
... build in commands
...
r register  (first name, last name, email, password)
l login (email, password)
f follow  (email)
l logout  ()
lu  list-users  ()
aa  add-activity  (type, location, distance)
la  list-activities ()
al  add-location  (activity-id, longitude, latitude)
ar  activity-report ()
ar  activity-report (byType: type)
lal list-activity-locations (activity-id)
lf  list-friends  ()
far friend-activity-report  (email)
uf  unfollow-friend ()
mf  message-friend  (email, message)
lm  list-messages ()
dlb distance-leader-board ()
dlbbt distance-leader-board-by-type (byType: type)
maf message-all-friends (message)
llb location-leader-board (location)

These are the commands for Assignment 2 - and are implemented as stubbs in PacemakerConsoleService class.

A scaled down implementation of the API is implemented in PacemakerAPI. It includes the primary features of the sample solution, simplified to exclude serialization.

The models are similar - but starttime and duration have been removed from the Activity class.

Initial Command Implementations

With the API implementation in place, we can make a start on some of the commands:

  @Command(description = "Register: Create an account for a new user")
  public void register(@Param(name = "first name") String firstName,
      @Param(name = "last name") String lastName,
      @Param(name = "email") String email, @Param(name = "password") String password) {
    console.renderUser(paceApi.createUser(firstName, lastName, email, password));
  }

  @Command(description = "List Users: List all users emails, first and last names")
  public void listUsers() {
    console.renderUsers(paceApi.getUsers());
  }

  @Command(description = "Login: Log in a registered user in to pacemaker")
  public void login(@Param(name = "email") String email,
      @Param(name = "password") String password) {
    Optional<User> user = Optional.fromNullable(paceApi.getUserByEmail(email));
    if (user.isPresent()) {
      if (user.get().password.equals(password)) {
        loggedInUser = user.get();
        console.println("Logged in " + loggedInUser.email);
        console.println("ok");
      } else {
        console.println("Error on login");
      }
    }
  }

  @Command(description = "Logout: Logout current user")
  public void logout() {
    console.println("Logging out " + loggedInUser.email);
    console.println("ok");
    loggedInUser = null;
  }

The above should permit the following interaction:

Welcome to pacemaker-console - ?help for instructions
pm> r homer simpson homer@simpson.com secret
+--------------------------------------+-----------+----------+-------------------+
|                  ID                  | FIRSTNAME | LASTNAME |       EMAIL       |
+--------------------------------------+-----------+----------+-------------------+
| 73cc563c-40b2-47a3-9acd-5a2471c4d7f9 |     homer |  simpson | homer@simpson.com |
+--------------------------------------+-----------+----------+-------------------+

ok
ok
pm> l homer@simpson.com secret
Logged in homer@simpson.com
ok
pm> l
Logging out homer@simpson.com
ok
pm>

Try it now to see if it works.

We can implement the add and list activities commands:

  @Command(description = "Add activity: create and add an activity for the logged in user")
  public void addActivity(
      @Param(name = "type") String type,
      @Param(name = "location") String location,
      @Param(name = "distance") double distance) {
    Optional<User> user = Optional.fromNullable(loggedInUser);
    if (user.isPresent()) {
      console
          .renderActivity(paceApi.createActivity(user.get().id, type, location, distance));
    }
  }

  @Command(description = "List Activities: List all activities for logged in user")
  public void listActivities() {
    Optional<User> user = Optional.fromNullable(loggedInUser);
    if (user.isPresent()) {
      console
          .renderActivities(paceApi.getActivities(user.get().id));
    }
  }

These commands should allow us to interact as follows (having logged in successfully):

pm> aa walk fridge 23
+--------------------------------------+------+----------+----------+-----------+----------+
|                  ID                  | TYPE | LOCATION | DISTANCE | STARTTIME | DURATION |
+--------------------------------------+------+----------+----------+-----------+----------+
| 2cc9b97d-346f-4d3f-96a7-ccfcf2351025 | walk |   fridge |       23 |      null |     null |
+--------------------------------------+------+----------+----------+-----------+----------+

ok
pm>aa walk tv 4
+--------------------------------------+------+----------+----------+-----------+----------+
|                  ID                  | TYPE | LOCATION | DISTANCE | STARTTIME | DURATION |
+--------------------------------------+------+----------+----------+-----------+----------+
| bfa408d8-be3e-4c88-99b7-0f50f3aa3408 | walk |       tv |        4 |      null |     null |
+--------------------------------------+------+----------+----------+-----------+----------+


ok
pm> la
+--------------------------------------+------+----------+----------+-----------+----------+
|                  ID                  | TYPE | LOCATION | DISTANCE | STARTTIME | DURATION |
+--------------------------------------+------+----------+----------+-----------+----------+
| 2cc9b97d-346f-4d3f-96a7-ccfcf2351025 | walk |   fridge |       23 |      null |     null |
| bfa408d8-be3e-4c88-99b7-0f50f3aa3408 | walt |       tv |        4 |      null |     null |
+--------------------------------------+------+----------+----------+-----------+----------+

ok
pm>

Exercises

Archive of the lab so far:

Exercise 1:

implement the Add Location Command

Exercise 2:

implement the List Location Command

Exercise 3:

Implement the Activity Report Command. There are two of these commands:

  • (a) taking no parameters - which sorts the activities by type

  • (b) talking a single parameter - the activity type. This command only lists activities of the specified type. However, they are to be sorted by distance, longest to shortest.

Note:

These exercises are solved in the next lab. Exercise 1 & 2 are relatively straightforward, and can be solved by looking at the sample solution to the assignment.

Exercise 3 (a) is also fairly straightforward, but exercise (b) might take a little more consideration.