• Lab-01 Eclipse & Java
  • Lab-02 CLI & Classes
  • Lab-03 Objects & Serialization
  • Lab-04 Testing
  • Lab-05 Refactoring
  • Lab-06 Maven
  • Lab-08 Skeleton
  • Lab-09 Rest API
  • Lab-10 Rest CLI
  • Lab-11 Rest Test
  • Lab-12 Kotlin
  • 8: SRP and TDD IV
  • Lab-08 Skeleton
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • Exercises
  • 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:

    • https://github.com/wit-computing-msc-2017/pacemaker-skeleton/releases/tag/lab08.end

    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.