Objectives

Refactor pacemaker to employ uuid instead of long ids. Unsure the tests as still passing as we make this transition. Make a start command line formatting features.

ID Management

Currently we are using Long id for our users and activitiy objects:

public class Activity implements Serializable
{ 
  static Long   counter = 0l;  
  public Long   id;
public class User implements Serializable
{ 
  static Long   counter = 0l;
  public Long   id;

This defines the structure of the various collections we are using:

public class PacemakerAPI
{
  private Map<String, User>   emailIndex = new HashMap<>();
  private Map<Long, User>     userIndex  = new HashMap<>();
  private Map<Long, Activity> activitiesIndex = new HashMap<>();

While this works (more or less), you may have noticed some flaws. These center around the counters, stored as static members of the Activity and User classes respectively.

The role of these counters is to facilitate generation of id's when we create new objects. For example:

  public User(String firstName, String lastName, String email, String password)
  {
    this.id        = counter++;
    ...
  }

We are simply increasing the counter by one every time a new user is created.

Can you see any flaw in this approach?

Specifically, if you restart the app, and load a previously saved model - what will be the value of these counters? What happens when you insert a new User for instance?

See if you can perform an experiment to exercise the above scenario.

Refactor User ID

You may have seen that the problem with the last step was the ids were reset to 0 when the app was launched. Thus, any new users or activities would overwrite already created users/activities - corrupting the data structure.

This is clearly a serious bug - one not picked up by our unit tests. In fact it is a difficult bug to pick up in the test infrastructure and you may only be able to reproduce using the command line.

Out solution will be to abandon this approach completely - and adopt a different strategy for managing ids. We can switch to using Immutable Universally unique Identifiers

A generate for these ids is available in the JDK:

This seems like a significant change - effecting many aspects of the application. However, because we have a suite of unit tests in place, we can evolve the application in an orderly manner.

First, change the User class:

...
import java.util.UUID;
...

public class User implements Serializable
{
  //static Long   counter = 0l;

  public String   id;
  ...

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

In the above we have commented out the counter - each new object receive a new unique id.

There will be errors throughout the application. We can start with the User Test class:

...
  @Test
  public void testIds()
  {
    Set<String> ids = new HashSet<>();
    for (User user : users)
    {
      ids.add(user.id);
    }
    assertEquals (users.length, ids.size());
  }
...

The above version should now compile successfully. We should also be able to run just this test (even though there are errors elsewhere). It should pass.

Refactor PacemakerAPI

The data structure can now altered to reflect the String id:

public class PacemakerAPI
{
  ...
  private Map<String, User>     userIndex  = new HashMap<>();
  ...

  @SuppressWarnings("unchecked")
  public void load() throws Exception
  {
    serializer.read();
    activitiesIndex = (Map<Long, Activity>) serializer.pop();
    emailIndex      = (Map<String, User>)   serializer.pop();
    userIndex       = (Map<String, User>)   serializer.pop();
  }
  ...
}

The PacemakerAPI class should now compile successfully.

We can now turn our attention to the unit tests - which have errors. These errors are as result of the API relying on Long ids. We will fix these in the PacemakerAPI class:

...
  public User getUser(String id) 
  {
     return userIndex.get(id);
  }
...
  public void deleteUser(String id) 
  {
   User user = userIndex.remove(id);
     emailIndex.remove(user.email);
  }
...
  public Activity createActivity(String id, String type, String location, double distance) 
  {
    ...
  }
...

The unit tests should now also compile successfully.

We should be in a position to run them all now - and they should pass.

Looking into our testdatstore.xml (if we dont delete it), we can see the new format of the id:

<object-stream>
  <java.util.Stack serialization="custom">
    <unserializable-parents/>
    <vector>
      <default>
        <capacityIncrement>0</capacityIncrement>
        <elementCount>3</elementCount>
        <elementData>
          <map>
            <entry>
              <string>071b8799-56e9-48cf-9d9e-565849c8456b</string>
              <models.User>
                <id>071b8799-56e9-48cf-9d9e-565849c8456b</id>
                <firstName>marge</firstName>
                <lastName>simpson</lastName>
                <email>marge@simpson.com</email>
                <password>secret</password>
                ...

Main

Main has a single error - which we can fix now:

 @Command(description="Add an activity")
  public void addActivity (@Param(name="user-id")  String id,
                           @Param(name="type")     String type,
                           @Param(name="location") String location,
                           @Param(name="distance") double distance)

The id parameter above has been changed from Long to String.

You should be able to run the app successfully now.

Git & Maven

In last weeks lab we referred to some useful utilities libraries for rendering tabular data:

In order to use this library, you will need to do two things:

  1. Clone the repository onto your workstation
  2. Rebuild the project, and include the jar file into your project.

For 1, you will need to have git installed, and then execute the following command:

git clone https://github.com/robinhowlett/java-ascii-table.git

This will replicate the project locally.

For 2, you must first install maven:

Make sure to follow the installation instructions for your platform. If installed correctly, the the following command should work correctly;

$ mvn -version

This should return something like this:

Apache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-03T20:39:06+01:00)
Maven home: /Users/edeleastar/dev/apache-maven-3.5.0
Java version: 1.8.0_60, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre
Default locale: en_IE, platform encoding: UTF-8
OS name: "mac os x", version: "10.13", arch: "x86_64", family: "mac"

Next weeks topic includes an in depth review of Maven, so installing the package now is a useful preparation.

Once installed, we can enter this command from inside the folder containing the project you cloned:

$ mvn package

This may take a few minutes to download and build all downstream components. It should finish successfully with something like this:

canning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building java-ascii-table 1.0
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ java-ascii-table ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/edeleastar/repos/modules/agile/labs/java-ascii-table/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.2:compile (default-compile) @ java-ascii-table ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ java-ascii-table ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/edeleastar/repos/modules/agile/labs/java-ascii-table/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.2:testCompile (default-testCompile) @ java-ascii-table ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ java-ascii-table ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ java-ascii-table ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.077 s
[INFO] Finished at: 2017-10-07T08:28:05+01:00
[INFO] Final Memory: 12M/309M
[INFO] ------------------------------------------------------------------------

It will have generated a jar file called java-ascii-table-1.0.jar into the ./target subfolder in the project. Copy this file intro your pacemaker/lib folder, and add the jar to your build path.

The project should look like this in Eclipse:

If you are having trouble with maven - here is a version of the jar file you can use without completing the above steps:

ascii-table

The readme for the library:

Contains just about enough guidance for using the library. Example 5 is simplest way in:

  Employee stud = new Employee("Sriram", 2, "Chess", false, 987654321.21d);
  Employee stud2 = new Employee("Sudhakar", 29, "Painting", true, 123456789.12d);
  List<Employee> students = Arrays.asList(stud, stud2);

  IASCIITableAware asciiTableAware = new CollectionASCIITableAware<Employee>(students, "name", "age", "married", "hobby", "salary"); 
  ASCIITable.getInstance().printTable(asciiTableAware);

Which will generate this output:

+----------+-----+---------+----------+----------------+
|   NAME   | AGE | MARRIED |   HOBBY  |     SALARY     |
+----------+-----+---------+----------+----------------+
|   Sriram |   2 |   false |    Chess | 987,654,321.21 |
| Sudhakar |  29 |    true | Painting | 123,456,789.12 |
+----------+-----+---------+----------+----------------+

This is relatively self explanatory. However, before engaging in our application, we need to introduce the following methods to the User class

  public String getFirstname() 
  {
    return firstName;
  }

  public String getLastname() 
  {
    return lastName;
  }

  public String getEmail() 
  {
    return email;
  }

These are accessors, which the library will use in constructing the output table.

Hers is a revised version of the getusers() method in the Main class:

  @Command(description="Get all users details")
  public void getUsers ()
  {
    List<User> userList = new ArrayList<User> (paceApi.getUsers());
    IASCIITableAware asciiTableAware = new CollectionASCIITableAware<User>(userList, "firstname", "lastname", "email"); 
    ASCIITable.getInstance().printTable(asciiTableAware);
  }

This should now support the following user interaction:

Welcome to pacemaker-console - ?help for instructions
pm> cu homer simpson homer@simpson.com secret
pm> gu
+-----------+----------+-------------------+
| FIRSTNAME | LASTNAME |       EMAIL       |
+-----------+----------+-------------------+
|     homer |  simpson | homer@simpson.com |
+-----------+----------+-------------------+

pm>

Looking at the above code again:

    // generate a List of Users from the Collection of users returned by our api. ascii-table expects a list
    List<User> userList = new ArrayList<User> (paceApi.getUsers());

    // Pass the list + the names of the field we wish to display. Note: there must be an accessor method in each 
    //  User object matching the names precisely
    IASCIITableAware asciiTableAware = new CollectionASCIITableAware<User>(userList, "firstname", "lastname", "email"); 

    // print the table
    ASCIITable.getInstance().printTable(asciiTableAware);
  • The component requires a List, so we create a list from the Collection returned from the getUsers() method.
  • We create a TableAware object - which is paramaterised by the type of our collection (User) + the user list itself, the the names of the fields we wish to appear in the table.
  • We print the table.

NB: Where we are using camel-case attributes names (e.g lastName), it is very important to use convert to lower case the second capital letter (the N in lastName) when composing the accessors. This is due to a bug in the library.

Exercises

Archive of the lab so far:

Exercise 1: UUID Ids for Activity & Location

Consider refactoring Activity and Location classes to use an ID based on UUID as we have implemented for the User class. Make sure you also rework the tests and get used to relying on the these tests to ensure the project is evolving in an orderly manner.

Exercise 2: ASCII Table

Introduce ASCII table into all commands, following the example of step 05

Exercise 3: Joda Time

Last week we referred you to this library:

This library arose as a result of inadequacies in date/time handing in the JDK. These have been largely resolved in Java 8, however there are some advantages to using Joda time:

  • It support for Duration is simpler and more usable that Java 8
  • Earlier JDK & Android support. The Joda Time library is available for a wider range of JDK versions.

There are, however, counter arguments. A useful discussion here:

Feel free to use either in your assignment.