Sections of the tutorial will continuously be published at this web page.

This tutorial serves to give you the practical knowledge required to execute the group project in true AGILE fashion.

1. Project Management Tools and Development Toolchain

1.1. Git

For our purposes, git is a software tool for tracking changes across application code files and for coordinating work between various programmers working on the same application source code during the application’s development and life cycle.

Glossary — Part 1:

  • Git is your version control software

  • GitHub hosts your repositories

  • A repository is a collection of files and their history

  • A commit is a saved state of the repository

Glossary — Part 2:

  • Working Directory: files being worked on right now

  • Staging area: files ready to be committed

  • Repository: A collection of commits

The main git workflow is as follows:

git remote

Install the Git version control system (VCS) from https://git-scm.com/downloads.

1.2. Gradle: A Build Framework and Brief Example

This section focuses on writing a Gradle (https://gradle.org/) build script that builds a single Gradle project referred to as Computation. The source code and tests for a Java application is available here: Computation.zip. It is your job to create/reorganize the folder called Computation, move sources and tests into that folder, and produce the Gradle build script build.gradle within this folder to automate the software build and testing process for this example project.

First, open a terminal, and ensure you have the newest version of Gradle (ver. 7.0+) installed with gradle --version.

Follow the steps below and add the snippets listed here to build.gradle, one after the other:

  1. Create the following folder structure and a new build.gradle (empty) file within the Computation folder:

Computation
├── build.gradle
└── src
    ├── main
    │   └── java
    │       ├── application
    │       │   └── CompApp.java
    │       ├── computation
    │       │   └── Computation.java
    │       └── view
    │           └── ComputationPage.java
    └── test
        └── java
            └── computation
                ├── AllTests.java
                ├── ComputationTestAddSubstract.java
                └── ComputationTestDivideMultiply.java
  1. Add the java and the application plugins to the build configuration script build.gradle.

    apply plugin: 'java'
    // This plugin has a predefined 'run' task that we can reuse to use Gradle to execute our application
    apply plugin: 'application'
  2. Add JUnit libraries to the dependencies section.

    repositories {
        mavenCentral()
    }
    dependencies {
        testImplementation "junit:junit:4.12"
    }
  3. Add and describe a new task compile(type: JavaCompile) to specify all source files (both application and test) and set the build/bin as destination dir to put all compiled class files in.

    task compile(type: JavaCompile) {
      classpath = sourceSets.main.compileClasspath
      classpath += sourceSets.test.runtimeClasspath
      sourceSets.test.java.outputDir = file('build/bin')
      sourceSets.main.java.outputDir = file('build/bin')
    }
    One can specify source sets and their variables the following way:
    /*
     * specifying sourceSets is not necessary in this case, since
     * we are applying the default folder structure assumed by Gradle
     */
    sourceSets {
      main {
        java { srcDir 'src/main/java' }
      }
      test {
        java { srcDir 'src/test/java'}
      }
    }
  4. Specify the main class and run the application.

    mainClassName='application.CompApp'

    In the command line issue gradle run

  5. Describe the jar Gradle task (defined by the java plugin) to produce an executable jar file into distributable/.

    jar {
      destinationDir=file('distributable')
      manifest {
        // It is smart to reuse the name of the main class variable instead of hardcoding it
        attributes "Main-Class": "$mainClassName"
      }
    }
The settings.gradle and its usage is to be shown later.

1.3. Spring Boot Projects and GitHub Project Management

This section touches on the use of Spring Boot CLI (command line interface) and explores the GitHub Project interface.

1.3.1. Spring Boot CLI Installation

Spring Boot is an open source framework for creating stand-alone Java-based applications. This is accomplished through the Spring Framework, which provides developers a comprehensive programming and configuration model for their Java applications.

We will return to the details of Spring later. For now, our only concern is setting it up.

  1. Install the Spring Boot CLI

1.3.2. Application Project Setup

  1. Create a new repository under your account on GitHub for an example application that we are going to develop throughout the semester. Name the repository eventregistration. See more on the specification of the application functionality later.
    image::figs/eventregistration-repo.png[Event Registration Repo]

  2. Clone it somewhere on your disk. We assume you cloned it to ~/git/eventregistration.

  3. Navigate to that folder in the terminal: cd ~/eventregistration.

  4. Create a project for the backend application using Spring Boot CLI in this repository.

    spring init \
     --build=gradle \
     --java-version=1.8 \
     --package=ca.mcgill.ecse321.eventregistration \
     --name=EventRegistration \
     --dependencies=web,data-jpa,postgresql \
     EventRegistration-Backend
    Backslashes in this snippet indicate linebreaks in this one liner command typed in the terminal. You can select and copy-paste this snippet as-is.
  5. Navigate to the EventRegistration-Backend folder

  6. For future use, locate the application.properties file in the src/ folder and add the following content:

    server.port=${PORT:8080}
    
    spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
    spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
  7. Locate the Java file containing the main application class (EventRegistrationApplication.java) and add the following content

    package ca.mcgill.ecse321.eventregistration;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @RestController
    @SpringBootApplication
    public class EventRegistrationApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(EventRegistrationApplication.class, args);
      }
    
      @RequestMapping("/")
      public String greeting(){
        return "Hello world!";
      }
    
    }
  8. Verify that it builds with gradle build -xtest.

  9. Commit and push the files of the new Spring project.

    git add .
    git status #verify the files that are staged for commit
    git commit -m "Initial commit of the backend application"
    git push

1.3.3. GitHub Repository Project

One of the core components of Agile development is being able to manage the development problem space. GitHub projects extends GitHub’s utility to make problem space management easy.

Any repository can be added to a project by selecting the Create New Project button located in the Project tab of the repository.

GitProj5

Below is the layout of the Basic KanBan Board.

GitProj6

By default there are three columns: To do, In progress and Done. More columns can be added by clicking the add column area.

Issues that exist already on the GitHub issue board can be searched by filter, then dragged and dropped into the board space. This is done by clicking Add cards at the top right corner.

GitHub project cards

It is generally prudent as a software engineer to automate away tedious tasks. You only need to automate it once. You can select various automation actions by clicking the ellipsis (…​) at the top right corner of each board.

GitHub project automation

That way when you create an issue on the issue board, close an issue etc. it is automatically added to the proper column.

GitHub project automation result

To help with better management of the project as you move through project phases, it is prudent to add Milestones. A new milestone can be created by selecting the Issues tab in the repository, and selecting the Milestones tab located next to the New issue button.

GitHub project milestone creation

To create a new milestone, select the New Milestone button. Then, fill out the form with an appropriate name, due date and description. Once your milestone has been created, you can attach issues to the milestone and see their progress by selecting the Milestones tab.

GitHub project milestone creation button

To help with better management of the project as you move through project phases, it is prudent to add Milestones. A new milestone can be created by selecting the Issues tab in the repository, and selecting the Milestones tab located next to the New issue button.

GitHub project milestone creation

To create a new milestone, select the New Milestone button. Then, fill out the form with an appropriate name, due date and description. Once your milestone has been created, you can attach issues to the milestone and see their progress by selecting the Milestones tab.

GitHub project milestone creation button
GitHub project milestone tracking

Finally, you can create issues to track. This is very straight forward, (select New issue button under the Issues tab of the repository), so the rest of this section will deal with some best practices when tracking issues in this course.

When creating a new issue it is imperative to be concise but also as descriptive as possible. All the issues you create should have a title, with a comment to describe the issue in detail.

All issues at the time of creation should be assigned to someone. You can always change this later. Label your issues. If none of the default labels fit, new labels can be created to meet your need. This is accomplished by selecting the Labels tab next to the Milestones tab under the Issues section. Then click the New Label button. Finally, assign your issue to the appropriate milestone and project.

GitHub project issue creation

For the purpose of tracking progress through the project, never ever ever delete issues. Issues should be closed and reopened as needed but never deleted. Even if a mistake was made during creation of an issue, issues can be edited by their creator.

GitHub project issue creation

If you’ve set everything up correctly. Your issue board should match your KanBan board. The KanBan board should be a snapshot of how the project is going. Nothing should be done manually here. All the manual labor of opening, moving and triaging issues should be done on the issue board.

GitHub project issue creation

1.4. GitHub Actions for Continuous Integration

Conitnuous integration (CI) is at the core of Agile development. The act of writing, testing and integrating incremental amounts of code often serves to detect errors, bugs and integration pain points sooner.

1.4.1. Setup CI Workflow and Template

  1. Go to your repository and select Actions in the repository menu tab.

  2. Select set up a workflow yourself linked just below the title. image::figs/githubactions1.png[]

  3. Modify the contents of the main.yml to have the following content:

name: Java CI with Gradle

on:
  push:
    branches: main
  pull_request:
    branches: main

jobs:
  build:

    runs-on: Windows-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up JDK 16
      uses: actions/setup-java@v2
      with:
        java-version: '16'
        distribution: 'adopt'
        cache: gradle
    - name: Validate Gradle wrapper
      uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
      working-directory: EventRegistration-Backend
    - name: Build with Gradle
      run: ./gradlew build
      working-directory: EventRegistration-Backend
The configurations in this YML file are used to determine the environment in which to run the specified GitHub action. Though not required, you will want these configurations to mirror the system you’re developing on if you implement build actions for the branches you are solely responsible for to make debugging errors easier. On branches with multiple developers, use configurations that have as much commonality as possible.

All that’s left to do is test to make sure it works. Go to local working copy of your repository and initiate a fetch, add, commit, status and push command in that order.

The result of workflow runs can be seen under the Actions tab of the repository.

Results of workflow run

To gain more insight into a workflow run, select the workflow run from the list . Even more insight can be gleaned from independent jobs by selecting them from the list of jjobs as well.

Workflow run detail
Job details

DEBUGGING EXERCISE: Why did the build fail? How would you fix it?

1.5. Heroku

Heroku is a cloud based PaaS (Platform as a Service) that allow the development, deployment and operation of applications. In this course, Heroku will be used as a host for our application backend (this includes the database).

1.5.1. Preparations

  1. Sign up/log in on Heroku by visiting https://www.heroku.com/.

  2. Install the command line client for Heroku: Heroku CLI

  3. Log in to Heroku CLI by opening a terminal and typing: heroku login -i. Enter your username and your password.

1.5.2. Creating a Heroku app

We are creating a Heroku application and deploying the Hello world! Spring example. Additionally, the steps below will make it possible to store multiple different applications in the same git repository and deploy them individually to Heroku. Steps will be shown through the example EventRegistration application, and should be adapted in the course project.

All actions described here for configuring Heroku applications using the Heroku CLI could also be done via the web UI.
  1. Once you are logged in with the Heroku-CLI, create a new Heroku application: in the root of the git repository of your repository (assumed to be ~/git/eventregistration), issue heroku create eventregistration-backend-<UNIQUE_ID> -n to create an application named "eventregistration-backend-<UNIQUE_ID>".

    In Heroku, the application name should be unique Heroku-wise, that is, each application in Heroku’s system should have a unique name. If you don’t provide a name parameter for the command, Heroku will randomly generate one. Make sure the value selected for <UNIQUE_ID> is alphanumeric and ends in letters. When entering a value for UNIQUE_ID replace "<UNIQUE_ID>" with this value. Do not include the "<>" in your command. This is a delimiter to indicate a parameter.
  2. Add the multi procfile and Gradle buildpacks to the app.

    heroku buildpacks:add -a eventregistration-backend-<UNIQUE_ID> https://github.com/heroku/heroku-buildpack-multi-procfile
    heroku buildpacks:add -a eventregistration-backend-<UNIQUE_ID> heroku/gradle
    Order is important.

1.5.3. Adding a database to the application

  1. Open the Heroku applications web page and go to Resources, then add the Heroku Postgres add-on.
    Heroku Postgres add-on

  2. Click the entry for Postgres within the list of add-ons, then go to Settings. You can see the database credentials there. Heroku Postgres add-on

Heroku Postgres add-on

The credentials are periodically updated and changed by Heroku, so make sure that you are using the actual credentials when manually connecting to the database. (E.g., during manual testing.)

1.5.4. Extending the build for the Heroku deployment environment

  1. Before deploying, a top level build.gradle and settings.gradle need to be created in the root of the repository (i.e., in ~/git/eventregistration)
    build.gradle:

    task stage () {
        dependsOn ':EventRegistration-Backend:assemble'
    }

    settings.gradle:

    include ':EventRegistration-Backend'
  2. Generate the Gradle wrapper with the newest Gradle version

    gradle wrapper --gradle-version 6.6.1
  3. Create a .gitignore file for the .gradle folder:
    .gitignore:

    .gradle/
  4. Add all new files to git

    git add .
    git status #make sure that files in .gradle/ are not added

    Expected output for git status:

    On branch master
    Your branch is ahead of 'origin/master' by 2 commits.
      (use "git push" to publish your local commits)
    
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
    	new file:   .gitignore
    	new file:   build.gradle
    	new file:   gradle/wrapper/gradle-wrapper.jar
    	new file:   gradle/wrapper/gradle-wrapper.properties
    	new file:   gradlew
    	new file:   gradlew.bat
    	new file:   settings.gradle

    Commit changes:

    git commit -m "Adding Gradle wrapper"

1.5.5. Supply application-specific setting for Heroku

  1. Within the EventRegistration-Backend folder, create a file called Procfile (not Procfile.txt, name it exactly Procfile) with the content:

    web: java -jar EventRegistration-Backend/build/libs/EventRegistration-Backend-0.0.1-SNAPSHOT.jar
  2. Add the Procfile to a new commit

  3. Configure the multi-procfile buildpack to find the Procfile (this is a terminal/cmd/powershell command):

    heroku config:add PROCFILE=EventRegistration-Backend/Procfile --app eventregistration-backend-<UNIQUE_ID>

1.5.6. Deploying the app

  1. Obtain and copy the Heroku Git URL

    heroku git:remote --app eventregistration-backend-<UNIQUE_ID> --remote backend-heroku

    Output:

    set git remote backend-heroku to https://git.heroku.com/eventregistration-backend-<UNIQUE_ID>.git
  2. Verify that the backend-heroku remote is successfully added besides origin with git remote -v. Output:

    backend-heroku	https://git.heroku.com/eventregistration-backend-123.git (fetch)
    backend-heroku	https://git.heroku.com/eventregistration-backend-123.git (push)
    origin	git@github.com:imbur/eventregistration.git (fetch)
    origin	git@github.com:imbur/eventregistration.git (push)
  3. Deploy your application with

    git push backend-heroku main
    If it fails to build, make sure you try understanding the output. Typical issue: buildpacks are not added/are not in the right order, failing tests, etc. (If you did not fix the build from the previous section, then pushing to the Heroku repository will fail, as Heroku cannot deploy an application it can’t build).
  4. If build and deployment are a success, visit the link provided in the build output. It may take some time (even 30-60 seconds) for the server to answer the first HTTP request, so be patient!

  5. Save your work to the GitHub repository, too: git push origin master
    Final layout of the files (only two directory levels are shown and hidden items are suppressed):

~/git/eventregistration
├── build.gradle
├── EventRegistration-Backend
│   ├── build
│   │   ├── classes
│   │   ├── libs
│   │   ├── resources
│   │   └── tmp
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   ├── gradlew
│   ├── gradlew.bat
│   ├── Procfile
│   ├── settings.gradle
│   └── src
│       ├── main
│       └── test
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── README.md
└── settings.gradle

1.6. A Quick Lesson In Debugging

Before moving on the the subsequent step it is necessary to ensure that the Event Registration project builds locally, on GitHub and on Heroku. Without this, we cannot continue building the project.

1.6.1. Debugging Flow

The general flow for debugging can be seen in the image below.

debug

When addressing an error/bug during a build process, building locally takes precedence for several reasons, the most important of which is mitigating error/bug propagation to others working on the same branch within the same project. In addition to this, it’s easier to access debugging logs locally than remotely on GitHub or Heroku, especially when an internet connection is unreliable.

The process then propagates to committing and pushing the changes to GitHub, checking that GitHub Actions successfully builds. If the build fails, it is possible to check the workflow log to see why. To gain more insight if the details are not enough, additional debug logging can be enabled.

Details

Finally, the project is deployed to Heroku. The same general process used for GitHub Actions can be used for Heroku. Heroku build logs can be seen when executing:

git push backend-heroku main
HerokuFail

or through the web interface under the Overview tab:

HerokuLog

A live demonstration of this process will be given in your tutorial section.

The _ suggested potential_ fixes implemented to allow the Event Registration Project to build locally, on GitHub and Heroku are as follows (follow the debugging flowchart after each step):

  • Ensure that the package names are identical in EventRegistrationApplication.java and EventRegistrationApplicationTests.java. The former is located in src>main>java and the latter in src>test>java directory.

  • Comment out the following lines in the EventRegistrationApplicationTests.java file (they will be reactivated later):

commentsTest
  • Modify the gradle.settings file at the root of the project to match the following (The value for the rootProject.name property should match the name of your root directory):

rootProject.name = 'eventregistrationfall2021'
include 'EventRegistration-Backend'
  • Add a file named system.properties to the root directory of your repository, and then update it with the following content (to maintain cohesion and smoothness in development, the java.runtime.version property should be the Heroku supported version closest to the version you have installed):

java.runtime.version=16.0.2

In the event that there are version discrepencies between members of a group, it could help designate one person to be responsible for deploying the application on Heroku.

2. Backend

2.1. The Domain Model

Our task is to build an event registration application that allows people to register for events. Before we can program anything we need to model the application.

For this, use the free modelling program Umple. Full documentation on how to use Umple can be found here.

Ensure that Umple is configured as shown in the image below:

UmpleConfig

The final model can be found below and is generated by the following code:

DomainModel
class RegistrationManager
{
}

class Person
{
  name;
}

class Event
{
  name;
  Date eventDate;
  Time startTime;
  Time endTime;
}

class Registration
{
  Integer id;
  }

association {
    1 RegistrationManager registrationManager <@>- 0..* Registration registrations;
}

association {
    1 RegistrationManager registrationManager <@>- 0..* Event events;
}

association {
    1 RegistrationManager registrationManager <@>- 0..* Person persons;
}

association {
   0..* Registration registration ->  1 Event event ;
}


association {
    0..* Registration registration ->  1 Person person;
}

Although the Umple generated Java code can be used, but is too verbose and not JPA compliant. You can see this by comparing the generated Java code to the code found in Model Classes.zip.

This is an exercise in being able to write JPA compliant code simply by looking at the domain model. A brief explanation for relevant JPA tags is here provided:

  • @Entity: Placed before the class declaration to signify an entity.

  • @Id: Placed before the get method for the attribute that will serve as the primary identifier for the class.

  • @OnetoMany or @ManytoOne: Placed before get method for attribute to signify the multiplicity in associative relationship between the current class and reference class. The first word is the multiplicity of the current class, with the other representing the multiplicity of the other calss. The cascade property being set to cascadeType.ALL ensures all operations of the defining class are persisted. The optional property being set to false means the association the tag defines must exist. In this case, the defining class cannot exist without knowledge of the referenced class.

Once your java code has been annotated, create a new folder under src>main>java called model and add the model files into that folder. Make sure your model files declare the package:

package ca.mcgill.ecse321.eventregistration.model;

2.2. Setting up a Spring-based Backend

You can download the Spring Tools Suite IDE from here.

2.2.1. Running the Backend Application from Eclipse

  1. Import the EventRegistration-Backend Spring Boot project as a Gradle project from File > Import…​ > Gradle > Existing Gradle project using the default settings. Select the previously generated Spring project folder as the root of the project.
    Gradle project import in Eclipse

  2. Ignore the bin folder.
    sts ignore bin

  3. Find the EventRegistrationApplication.java source file, then right click and select Run As > Spring Boot App. The application will fail to start, since the database is not yet configured, but this action will create an initial run configuration. Example console output (fragment):

    [...]
    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
    
    Reason: Failed to determine a suitable driver class
    [...]
  4. Obtain the database URL to access the database remotely, e.g., by opening up a terminal and running:

heroku run echo \$JDBC_DATABASE_URL --app=<YOUR_BACKEND_APP_NAME>

If you are on Windows, open up command line or powershell and run:

heroku run echo '$JDBC_DATABASE_URL' --app=<YOUR_BACKEND_APP_NAME>
  1. In Eclipse, open the EventRegistration-Backend - EventregistrationApplication run configuration page and add an environment variable called SPRING_DATASOURCE_URL with the value obtained in the previous step.
    Adding env var to run config

  2. Add the spring.jpa.hibernate.ddl-auto=update to application.properties. The database content along with the tables this way will be deleted (as necessary) then re-created each time your application starts.

    In production, the value of this property should be none (instead of update). Possible values are none, create, validate, and update.
  3. If needed: troubleshooting:

    • If you get an error message saying something similar to createClob() is not yet implemented, then you can try setting the spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true variable in your application.properties. It could be a workaround a workaround for an issue with Postgres.

    • Sometimes environment variables don’t work with Spring apps. In this case you can set the spring.datasource.url, the spring.datasource.username, and the spring.datasource.password variables in the application properties as an alternative to setting the SPRING_DATASOURCE_URL environment variable.

    • Make sure no other apps are running on localhost:8080. You can test it by opening the browser and entering localhost:8080 as the address.

2.2.2. Spring Transactions

  1. Verify the contents of the EventRegistrationApplication class:

    package ca.mcgill.ecse321.eventregistration;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.SpringApplication;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @RestController
    @SpringBootApplication
    public class EventRegistrationApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(EventRegistrationApplication.class, args);
    	}
    
    	@RequestMapping("/")
    	public String greeting() {
    		return "Hello world!";
    	}
    }
  2. Create a new package in src/main/java and name it ca.mcgill.ecse321.eventregistration.dao.

  3. Create the EventRegistrationRepository class within this new package

    package ca.mcgill.ecse321.eventregistration.dao;
    
    import java.sql.Date;
    import java.sql.Time;
    import java.util.List;
    
    import javax.persistence.EntityManager;
    import javax.persistence.TypedQuery;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Transactional;
    
    import ca.mcgill.ecse321.eventregistration.model.Person;
    import ca.mcgill.ecse321.eventregistration.model.Event;
    
    @Repository
    public class EventRegistrationRepository {
    
    	@Autowired
    	EntityManager entityManager;
    
    	@Transactional
    	public Person createPerson(String name) {
    		Person p = new Person();
    		p.setName(name);
    		entityManager.persist(p);
    		return p;
    	}
    
    	@Transactional
    	public Person getPerson(String name) {
    		Person p = entityManager.find(Person.class, name);
    		return p;
    	}
    
    	@Transactional
    	public Event createEvent(String name, Date date, Time startTime, Time endTime) {
    		Event e = new Event();
    		e.setName(name);
    		e.setDate(date);
    		e.setStartTime(startTime);
    		e.setEndTime(endTime);
    		entityManager.persist(e);
    		return e;
    	}
    
    	@Transactional
    	public Event getEvent(String name) {
    		Event e = entityManager.find(Event.class, name);
    		return e;
    	}
    
    }
  4. Add a new method that gets all events before a specified date (deadline). Use a typed query created from an SQL command:

    @Transactional
    public List<Event> getEventsBeforeADeadline(Date deadline) {
    	TypedQuery<Event> q = entityManager.createQuery("select e from Event e where e.date < :deadline",Event.class);
    	q.setParameter("deadline", deadline);
    	List<Event> resultList = q.getResultList();
    	return resultList;
    }
To try the methods, you can create a JUnit test under src/test/java. Currently the methods in EventRegistrationRepository directly access the objects stored in the database via the EntityManager instance and these methods should implement both database operations and service business logic (including input validation — which we omitted in this part). In later sections, however, we will see how we can easily separate the database access and the service business logic in Spring applications.

2.2.3. Debugging: connecting to the database using a client

There are cases when a developer wants to know the contents of the database. In this case, a database client program can be used to access the database schema and table contents. Here are the general steps to access the Postgres database provided by Heroku:

  1. Obtain the database URL to access the database remotely, e.g., by opening up a terminal and running: heroku run echo \$JDBC_DATABASE_URL --app=<YOUR_BACKEND_APP_NAME>.

  2. The returned value follows the format that holds all main important parameters that are needed for accessing the database server:

    jdbc:postgresql://<HOST>:<PORT>/<DATABASE_NAME>?user=<USERNAME>&password=<PASSWORD>&sslmode=require

    These parameters are:

    • Database host: the URL for the server

    • Port: the por on which the DB server is listening

    • Database name: the first section after the URL

    • Username: the first parameter value in the provided URL

    • Password: the second parameter value in the provided URL

  3. With these parameters you can use any Postgres client you prefer to connect to the database. Here is an example for such a connection from Linux using postgres-client:

    $> psql postgresql://ec2-54-243-223-245.compute-1.amazonaws.com:5432/d4412g60aaboa7?user=hdjnflfirvkmmr
    Password:
    psql (10.6 (Ubuntu 10.6-0ubuntu0.18.04.1))
    SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
    Type "help" for help.
    
    d4412g60aaboa7=> \dt
                              List of relations
     Schema |                Name                | Type  |     Owner
    --------+------------------------------------+-------+----------------
     public | event                              | table | hdjnflfirvkmmr
     public | person                             | table | hdjnflfirvkmmr
     public | registration                       | table | hdjnflfirvkmmr
     public | registration_manager               | table | hdjnflfirvkmmr
     public | registration_manager_events        | table | hdjnflfirvkmmr
     public | registration_manager_persons       | table | hdjnflfirvkmmr
     public | registration_manager_registrations | table | hdjnflfirvkmmr
    (7 rows)
    
    d4412g60aaboa7=> select * from event ;
     name |    date    | end_time | start_time
    ------+------------+----------+------------
     e1   | 3899-10-09 | 12:00:00 | 10:00:00
    (1 row)
    
    d4412g60aaboa7=> \q
    $>

2.3. CRUD Repositories

Previously, in the ca.mcgill.ecse321.eventregistration.dao.EventRegistrationRepository class we used an instance of javax.persistence.EntityManager from Hibernate to directly implement the required operations related to saving/retrieving data to/from a database (Create, Read, Update, and Delete operations, shortly, CRUD). This section will introduce the Spring framework’s inbuilt support for such CRUD operations via the org.springframework.data.repository.CrudRepository interface and will show how to use such repositories to implement your use cases in so-called service classes.

If you would like to, you can obtain a version of the project that already has the code for the backend and model classes from the previous tutorials here.

2.3.1. Creating a CRUD Repository

  1. Create a new interface PersonRepository in the ca.mcgill.ecse321.eventregistration.dao package and extend the CrudRepository<Person, String> interface

  2. Create a new method Person findByName(String name)

    package ca.mcgill.ecse321.eventregistration.dao;
    
    import org.springframework.data.repository.CrudRepository;
    
    import ca.mcgill.ecse321.eventregistration.model.Person;
    
    public interface PersonRepository extends CrudRepository<Person, String>{
    
    	Person findPersonByName(String name);
    
    }
  3. Since Spring supports automated JPA Query creation from method names (see possible language constructs here) we don’t need to implement the interface manually, Spring JPA will create the corresponding queries runtime! This way we don’t need to write SQL queries either.

  4. Create interfaces for the Event and Registration classes as well
    EventRepository.java:

    package ca.mcgill.ecse321.eventregistration.dao;
    
    import org.springframework.data.repository.CrudRepository;
    
    import ca.mcgill.ecse321.eventregistration.model.Event;
    
    public interface EventRepository extends CrudRepository<Event, String> {
    
    	Event findEventByName(String name);
    
    }

    RegistrationRepository.java:

    package ca.mcgill.ecse321.eventregistration.dao;
    
    import java.util.List;
    
    import org.springframework.data.repository.CrudRepository;
    
    import ca.mcgill.ecse321.eventregistration.model.Event;
    import ca.mcgill.ecse321.eventregistration.model.Person;
    import ca.mcgill.ecse321.eventregistration.model.Registration;
    
    public interface RegistrationRepository extends CrudRepository<Registration, Integer> {
    
    	List<Registration> findByPerson(Person personName);
    
    	boolean existsByPersonAndEvent(Person person, Event eventName);
    
    	Registration findByPersonAndEvent(Person person, Event eventName);
    
    }
  5. Finally, create a new package in src/main/java and name it ca.mcgill.ecse321.eventregistration.model. Place all the model classes generated in the Step 2.4. By now, all the errors in all other packages should be resolved automatically. In the end, you should have the given below structure in terms of packages.

Package Structure

2.4. Unit Testing Persistence in the Backend

  1. In a fresh Spring Boot project, there is already a single test class EventRegistrationApplicationTests in the src/test/java folder that looks like the following:

    package ca.mcgill.ecse321.eventregistration;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class EventRegistrationApplicationTests {
    
    	@Test
    	void contextLoads() {
    	}
    
    }
  2. Run this test that checks if the application can successfully load by right clicking on the class → Run as…​ → JUnit test

    You need to set the SPRING_DATASOURCE_URL for the test run configuration as well if you use an environment variable to set datasource URL (see earlier).
  3. Add a new test class ca.mcgill.ecse321.eventregistration.dao.TestEventRegistrationPersistence and implement tests for the persistence

    package ca.mcgill.ecse321.eventregistration.dao;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    
    import java.sql.Date;
    import java.sql.Time;
    import java.time.LocalDate;
    import java.time.LocalTime;
    import java.time.Month;
    
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    
    import ca.mcgill.ecse321.eventregistration.model.Event;
    import ca.mcgill.ecse321.eventregistration.model.Person;
    import ca.mcgill.ecse321.eventregistration.model.Registration;
    
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    public class TestEventRegistrationPersistence {
    
    	@Autowired
    	private PersonRepository personRepository;
    	@Autowired
    	private EventRepository eventRepository;
    	@Autowired
    	private RegistrationRepository registrationRepository;
    
    	@AfterEach
    	public void clearDatabase() {
    		// Fisrt, we clear registrations to avoid exceptions due to inconsistencies
    		registrationRepository.deleteAll();
    		// Then we can clear the other tables
    		personRepository.deleteAll();
    		eventRepository.deleteAll();
    	}
    
    	@Test
    	public void testPersistAndLoadPerson() {
    		String name = "TestPerson";
    		// First example for object save/load
    		Person person = new Person();
    		// First example for attribute save/load
    		person.setName(name);
    		personRepository.save(person);
    
    		person = null;
    
    		person = personRepository.findPersonByName(name);
    		assertNotNull(person);
    		assertEquals(name, person.getName());
    	}
    
    	@Test
    	public void testPersistAndLoadEvent() {
    		String name = "ECSE321 Tutorial";
    		Date date = java.sql.Date.valueOf(LocalDate.of(2020, Month.JANUARY, 31));
    		Time startTime = java.sql.Time.valueOf(LocalTime.of(11, 35));
    		Time endTime = java.sql.Time.valueOf(LocalTime.of(13, 25));
    		Event event = new Event();
    		event.setName(name);
    		event.setDate(date);
    		event.setStartTime(startTime);
    		event.setEndTime(endTime);
    		eventRepository.save(event);
    
    		event = null;
    
    		event = eventRepository.findEventByName(name);
    
    		assertNotNull(event);
    		assertEquals(name, event.getName());
    		assertEquals(date, event.getDate());
    		assertEquals(startTime, event.getStartTime());
    		assertEquals(endTime, event.getEndTime());
    	}
    
    	@Test
    	public void testPersistAndLoadRegistration() {
    		String personName = "TestPerson";
    		Person person = new Person();
    		person.setName(personName);
    		personRepository.save(person);
    
    		String eventName = "ECSE321 Tutorial";
    		Date date = java.sql.Date.valueOf(LocalDate.of(2020, Month.JANUARY, 31));
    		Time startTime = java.sql.Time.valueOf(LocalTime.of(11, 35));
    		Time endTime = java.sql.Time.valueOf(LocalTime.of(13, 25));
    		Event event = new Event();
    		event.setName(eventName);
    		event.setDate(date);
    		event.setStartTime(startTime);
    		event.setEndTime(endTime);
    		eventRepository.save(event);
    
    		Registration reg = new Registration();
    		int regId = 1;
    		// First example for reference save/load
    		reg.setId(regId);
    		reg.setPerson(person);
    		reg.setEvent(event);
    		registrationRepository.save(reg);
    
    		reg = null;
    
    		reg = registrationRepository.findByPersonAndEvent(person, event);
    		assertNotNull(reg);
    		assertEquals(regId, reg.getId());
    		// Comparing by keys
    		assertEquals(person.getName(), reg.getPerson().getName());
    		assertEquals(event.getName(), reg.getEvent().getName());
    	}
    
    }
  4. In the end, you should have the given below structure in terms of packages.

Package Structure

  1. Run this test suite by right clicking on the class → Run as…​ → JUnit test. Again, don’t forget to set the SPRING_DATASOURCE_URL value for the run configuration.

2.5. Creating RESTful Web Services in Spring

The following steps provide guidance on (1) implementing business logic that implements the required functionality (classes annotated with @Service) and (2) exposing them using a REST API in the context of the Event Registration Application (classes annotated with @RestController).

2.5.1. Implementing Service Methods

We implement use-cases in service classes by using the DAOs for each data type of the domain model.

  1. In src/main/java, create a new package ca.mcgill.ecse321.eventregistration.service.

  2. In this package, create the EventRegistrationService class as shown below

    package ca.mcgill.ecse321.eventregistration.service;
    
    import java.sql.Date;
    import java.sql.Time;
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import ca.mcgill.ecse321.eventregistration.dao.EventRepository;
    import ca.mcgill.ecse321.eventregistration.dao.PersonRepository;
    import ca.mcgill.ecse321.eventregistration.dao.RegistrationRepository;
    import ca.mcgill.ecse321.eventregistration.model.Event;
    import ca.mcgill.ecse321.eventregistration.model.Person;
    import ca.mcgill.ecse321.eventregistration.model.Registration;
    
    @Service
    public class EventRegistrationService {
    
    	@Autowired
    	EventRepository eventRepository;
    	@Autowired
    	PersonRepository personRepository;
    	@Autowired
    	RegistrationRepository registrationRepository;
    
    	@Transactional
    	public Person createPerson(String name) {
    		Person person = new Person();
    		person.setName(name);
    		personRepository.save(person);
    		return person;
    	}
    
    	@Transactional
    	public Person getPerson(String name) {
    		Person person = personRepository.findPersonByName(name);
    		return person;
    	}
    
    	@Transactional
    	public List<Person> getAllPersons() {
    		return toList(personRepository.findAll());
    	}
    
    	@Transactional
    	public Event createEvent(String name, Date date, Time startTime, Time endTime) {
    		Event event = new Event();
    		event.setName(name);
    		event.setDate(date);
    		event.setStartTime(startTime);
    		event.setEndTime(endTime);
    		eventRepository.save(event);
    		return event;
    	}
    
    	@Transactional
    	public Event getEvent(String name) {
    		Event event = eventRepository.findEventByName(name);
    		return event;
    	}
    
    	@Transactional
    	public List<Event> getAllEvents() {
    		return toList(eventRepository.findAll());
    	}
    
    	@Transactional
    	public Registration register(Person person, Event event) {
    		Registration registration = new Registration();
    		registration.setId(person.getName().hashCode() * event.getName().hashCode());
    		registration.setPerson(person);
    		registration.setEvent(event);
    
    		registrationRepository.save(registration);
    
    		return registration;
    	}
    
    	@Transactional
    	public List<Registration> getAllRegistrations(){
    		return toList(registrationRepository.findAll());
    	}
    
    	@Transactional
    	public List<Event> getEventsAttendedByPerson(Person person) {
    		List<Event> eventsAttendedByPerson = new ArrayList<>();
    		for (Registration r : registrationRepository.findByPerson(person)) {
    			eventsAttendedByPerson.add(r.getEvent());
    		}
    		return eventsAttendedByPerson;
    	}
    
    	private <T> List<T> toList(Iterable<T> iterable){
    		List<T> resultList = new ArrayList<T>();
    		for (T t : iterable) {
    			resultList.add(t);
    		}
    		return resultList;
    	}
    
    }

2.5.2. Exposing Service Functionality via a RESTful API

Building a RESTful Web Service Using a Controller and Data Transfer Objects
  1. We first create a new package ca.mcgill.ecse321.eventregistration.controller in EventRegistration-Backend and then create EventRegistrationRestController class inside it. We add the annotation @RestController to the controller class so that HTTP requests can be dispacthed to EventRegistrationRestController class. In addition, we enabled the Cross-Origin Resource Sharing for any domain using the @CrossOrigin annotation on the REST controller class.

    package ca.mcgill.ecse321.eventregistration.controller;
    
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.RestController;
    
    @CrossOrigin(origins = "*")
    @RestController
    public class EventRegistrationRestController {
    
    	@Autowired
    	private EventRegistrationService service;
    
    }
  2. We further create another package ca.mcgill.ecse321.eventregistration.dto and create the below Data Transfer Object (DTO) classes inside that package. First we create EventDto.java.

    Data Access Object (DAO) != Data Transfer Object (DTO). These two are completely separate concepts, as you will also see below. These two should not be confused with each other.
    package ca.mcgill.ecse321.eventregistration.dto;
    
    import java.sql.Date;
    import java.sql.Time;
    
    public class EventDto {
    
    	private String name;
    	private Date eventDate;
    	private Time startTime;
    	private Time endTime;
    
    	public EventDto() {
    	}
    
    	public EventDto(String name) {
    		this(name, Date.valueOf("1971-01-01"), Time.valueOf("00:00:00"), Time.valueOf("23:59:59"));
    	}
    
    	public EventDto(String name, Date eventDate, Time startTime, Time endTime) {
    		this.name = name;
    		this.eventDate = eventDate;
    		this.startTime = startTime;
    		this.endTime = endTime;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public Date getEventDate() {
    		return eventDate;
    	}
    
    	public Time getStartTime() {
    		return startTime;
    	}
    
    	public Time getEndTime() {
    		return endTime;
    	}
    
    }
  3. Next, we create PersonDto Java class.

    package ca.mcgill.ecse321.eventregistration.dto;
    
    import java.util.Collections;
    import java.util.List;
    
    public class PersonDto {
    
    	private String name;
    	private List<EventDto> events;
    
    	public PersonDto() {
    	}
    
    	@SuppressWarnings("unchecked")
    	public PersonDto(String name) {
    		this(name, Collections.EMPTY_LIST);
    	}
    
    	public PersonDto(String name, List<EventDto> arrayList) {
    		this.name = name;
    		this.events = arrayList;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public List<EventDto> getEvents() {
    		return events;
    	}
    
    	public void setEvents(List<EventDto> events) {
    		this.events = events;
    	}
    
    }
  4. Finally, we create RegistrationDto Java class.

    package ca.mcgill.ecse321.eventregistration.dto;
    
    public class RegistrationDto {
    
    	private PersonDto person;
    	private EventDto event;
    
    	public RegistrationDto() {
    	}
    
    	public RegistrationDto(PersonDto person, EventDto event) {
    		this.person = person;
    		this.event = event;
    	}
    
    	public PersonDto getperson() {
    		return person;
    	}
    
    	public void setperson(PersonDto person) {
    		this.person = person;
    	}
    
    	public EventDto getEvent() {
    		return event;
    	}
    
    	public void setEvent(EventDto event) {
    		this.event = event;
    	}
    }
  5. We start adding the methods in the EventRegistrationRestController class. Also, we will add annotaions to map HTTP requests.

    In Spring Tools Suite (a.k.a. Eclipse), you can organize Java imports with kbd:[Ctrl+Shift+o]
    @GetMapping(value = { "/persons", "/persons/" })
    public List<PersonDto> getAllPersons() {
    	return service.getAllPersons().stream().map(p -> convertToDto(p)).collect(Collectors.toList());
    }
    
    @PostMapping(value = { "/persons/{name}", "/persons/{name}/" })
    public PersonDto createPerson(@PathVariable("name") String name) throws IllegalArgumentException {
    	Person person = service.createPerson(name);
    	return convertToDto(person);
    }

    The @RequestMapping annotation is used to map HTTP requests to Spring Controller methods. Since, @RequestMapping maps all HTTP operations by default. We can use @GetMapping, @PostMapping and so forth to narrow this mapping to specific HTTP operations.

    Moreover, in the above snippet, we use the value parameter of @PathVariable annotation to bind the value of the query string parameter name into the name parameter of the createPerson() method.

  6. You can add other methods similarly with appropriate mappings.

    @PostMapping(value = { "/events/{name}", "/events/{name}/" })
    public EventDto createEvent(@PathVariable("name") String name, @RequestParam Date date,
    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.TIME, pattern = "HH:mm") LocalTime startTime,
    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.TIME, pattern = "HH:mm") LocalTime endTime)
    throws IllegalArgumentException {
    	Event event = service.createEvent(name, date, Time.valueOf(startTime), Time.valueOf(endTime));
    	return convertToDto(event);
    }
    
    @GetMapping(value = { "/events", "/events/" })
    public List<EventDto> getAllEvents() {
    	List<EventDto> eventDtos = new ArrayList<>();
    	for (Event event : service.getAllEvents()) {
    		eventDtos.add(convertToDto(event));
    	}
    	return eventDtos;
    }
    
    @PostMapping(value = { "/register", "/register/" })
    public RegistrationDto registerPersonForEvent(@RequestParam(name = "person") PersonDto pDto,
    	@RequestParam(name = "event") EventDto eDto) throws IllegalArgumentException {
    	Person p = service.getPerson(pDto.getName());
    	Event e = service.getEvent(eDto.getName());
    
    	Registration r = service.register(p, e);
    	return convertToDto(r, p, e);
    }
    
    @GetMapping(value = { "/registrations/person/{name}", "/registrations/person/{name}/" })
    public List<EventDto> getEventsOfPerson(@PathVariable("name") PersonDto pDto) {
    	Person p = convertToDomainObject(pDto);
    	return createEventDtosForPerson(p);
    }
    
    @GetMapping(value = { "/events/{name}", "/events/{name}/" })
    public EventDto getEventByName(@PathVariable("name") String name) throws IllegalArgumentException {
    	return convertToDto(service.getEvent(name));
    }
    
    private EventDto convertToDto(Event e) {
    	if (e == null) {
    		throw new IllegalArgumentException("There is no such Event!");
    	}
    	EventDto eventDto = new EventDto(e.getName(),e.getDate(),e.getStartTime(),e.getEndTime());
    	return eventDto;
    }
    
    private PersonDto convertToDto(Person p) {
    	if (p == null) {
    		throw new IllegalArgumentException("There is no such Person!");
    	}
    	PersonDto personDto = new PersonDto(p.getName());
    	personDto.setEvents(createEventDtosForPerson(p));
    	return personDto;
    }
    
    private RegistrationDto convertToDto(Registration r, Person p, Event e) {
    	EventDto eDto = convertToDto(e);
    	PersonDto pDto = convertToDto(p);
    	return new RegistrationDto(pDto, eDto);
    }
    
    private Person convertToDomainObject(PersonDto pDto) {
    	List<Person> allPersons = service.getAllPersons();
    	for (Person person : allPersons) {
    		if (person.getName().equals(pDto.getName())) {
    			return person;
    		}
    	}
    	return null;
    }
    
    private List<EventDto> createEventDtosForPerson(Person p) {
    	List<Event> eventsForPerson = service.getEventsAttendedByPerson(p);
    	List<EventDto> events = new ArrayList<>();
    	for (Event event : eventsForPerson) {
    		events.add(convertToDto(event));
    	}
    	return events;
    }
Trying (Smoke Testing of) the Application

We can see if our application is able to respond to HTTP requests using, e.g., the Postman (Chrome), RESTClient browser plugin (Firefox), Advanced Rest Client (Firefox), or the command line tool called curl.

Once you launch the client, you can specify the path and select the method as shown in the below figures.

Post method on REST Client

Once we use POST, the record is persisted and then we can use the GET method to retrive the same.

GET method on REST Client

Similary, we can try other methods as well.

2.5.3. Spring Data - an Alternative Way to Expose Application Data via a RESTful Interface

The advantage of using Spring Data Rest is that it can remove a lot of boilerplate compared to the previous sections. Spring would automatically create endpoints for classes, such as /events and /person in the Event Registration example. In this case, implementing proper error handling may require some extra effort (not discussed here).

This section presents an alternative way of exposing your data via a REST API. You do not have to use this method if you do not think that it fits your design.
  1. Add the dependency 'spring-boot-starter-data-rest' in build.gradle file of your backend. It is required to expose Spring Data repositories over REST using Spring Data REST. Update your dependencies section as shown below:

    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    
    	runtimeOnly 'org.postgresql:postgresql'
    	testImplementation('org.springframework.boot:spring-boot-starter-test') {
    		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    	}
    }
  2. We have already added the dependency spring-boot-starter-data-rest exposes DAOs over RESTful endpoints using Spring Data REST.

  3. Next, we can go to repository interfaces and add a @RepositoryRestResource annotaion.

    // REST endpoint specification
    @RepositoryRestResource(collectionResourceRel = "person_data", path = "person_data")
    public interface PersonRepository extends CrudRepository<Person, String>{
    
    	Person findPersonByName(String name);
    
    }
  4. Finally, we can access this REST API (http://localhost:8080/person_data) in the browser or REST Client and will receive the JSON as shown below.

    Post method on REST Client

  5. Exercise: turn on this Spring Data JPA feature for events and registrations, too.

  6. Question: what information do we see in the response?

2.6. Unit Testing Service Methods in Backend

2.6.1. Service Unit Testing Setup with Mockito

We need extra dependencies to support testing of the business methods in isolation.

  1. Add the following dependencies to the project:

    testImplementation 'org.mockito:mockito-core:2.+'
    testImplementation 'org.mockito:mockito-junit-jupiter:2.18.3'
    
    testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
    Finding configuration settings for your Gradle/Maven projects is very simple by searaching for them on MVNRepository: https://mvnrepository.com/
  2. If you also would like to run your project from Eclipse, add an additional dependency:

    testImplementation 'org.junit.platform:junit-platform-launcher:1.4.1'
  3. Create a test class (in case you don’t already have one) TestEventRegistrationService in the corresponding package under src/test/java:

    package ca.mcgill.ecse321.eventregistration.service;
    
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.junit.jupiter.MockitoExtension;
    
    
    @ExtendWith(MockitoExtension.class)
    public class TestEventRegistrationService {
    
    }
  4. Build your project to ensure its dependencies are correctly loaded.

2.6.2. Implementing Unit Tests for Service Class

  1. Add the following static imports for methods:

    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertNull;
    import static org.junit.jupiter.api.Assertions.fail;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.ArgumentMatchers.anyString;
    import static org.mockito.Mockito.lenient;
    import static org.mockito.Mockito.when;
  2. Add the following imports to the test class:

    import java.sql.Date;
    import java.sql.Time;
    import java.time.LocalTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Calendar;
    
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.invocation.InvocationOnMock;
    import org.mockito.junit.jupiter.MockitoExtension;
    import org.mockito.stubbing.Answer;
    
    import ca.mcgill.ecse321.eventregistration.dao.EventRepository;
    import ca.mcgill.ecse321.eventregistration.dao.PersonRepository;
    import ca.mcgill.ecse321.eventregistration.dao.RegistrationRepository;
    import ca.mcgill.ecse321.eventregistration.model.Event;
    import ca.mcgill.ecse321.eventregistration.model.Person;
    import ca.mcgill.ecse321.eventregistration.model.Registration;
  3. Create the DAO mock for person

    @Mock
    private PersonRepository personDao;
    
    @InjectMocks
    private EventRegistrationService service;
    
    private static final String PERSON_KEY = "TestPerson";
    
    @BeforeEach
    public void setMockOutput() {
        lenient().when(personDao.findPersonByName(anyString())).thenAnswer( (InvocationOnMock invocation) -> {
            if(invocation.getArgument(0).equals(PERSON_KEY)) {
                Person person = new Person();
                person.setName(PERSON_KEY);
                return person;
            } else {
                return null;
            }
        });
    }
  4. Add test cases from the complete test suite that is available from here.

  5. Run the tests as JUnit/Gradle tests and interpret the test error messages! You should see only a few (at least one) tests passing.

  6. Update the implementation (i.e., replace the current service method codes with the ones provided below) of the following methods with input validation in the EventRegistrationService service class to make the tests pass (we are rapid simulating a TDD process — TDD stands for Test-Driven Development)

    @Transactional
    public Person createPerson(String name) {
    	if (name == null || name.trim().length() == 0) {
    		throw new IllegalArgumentException("Person name cannot be empty!");
    	}
    	Person person = new Person();
    	person.setName(name);
    	personRepository.save(person);
    	return person;
    }
    
    @Transactional
    public Person getPerson(String name) {
        if (name == null || name.trim().length() == 0) {
            throw new IllegalArgumentException("Person name cannot be empty!");
        }
        Person person = personRepository.findPersonByName(name);
        return person;
    }
    
    @Transactional
    public Event getEvent(String name) {
        if (name == null || name.trim().length() == 0) {
            throw new IllegalArgumentException("Event name cannot be empty!");
        }
        Event event = eventRepository.findEventByName(name);
        return event;
    }
    
    @Transactional
    public Event createEvent(String name, Date date, Time startTime, Time endTime) {
        // Input validation
        String error = "";
        if (name == null || name.trim().length() == 0) {
            error = error + "Event name cannot be empty! ";
        }
        if (date == null) {
            error = error + "Event date cannot be empty! ";
        }
        if (startTime == null) {
            error = error + "Event start time cannot be empty! ";
        }
        if (endTime == null) {
            error = error + "Event end time cannot be empty! ";
        }
        if (endTime != null && startTime != null && endTime.before(startTime)) {
            error = error + "Event end time cannot be before event start time!";
        }
        error = error.trim();
        if (error.length() > 0) {
            throw new IllegalArgumentException(error);
        }
    
        Event event = new Event();
        event.setName(name);
        event.setDate(date);
        event.setStartTime(startTime);
        event.setEndTime(endTime);
        eventRepository.save(event);
        return event;
    }
    
    @Transactional
    public Registration register(Person person, Event event) {
        String error = "";
        if (person == null) {
            error = error + "Person needs to be selected for registration! ";
        } else if (!personRepository.existsById(person.getName())) {
            error = error + "Person does not exist! ";
        }
        if (event == null) {
            error = error + "Event needs to be selected for registration!";
        } else if (!eventRepository.existsById(event.getName())) {
            error = error + "Event does not exist!";
        }
        if (registrationRepository.existsByPersonAndEvent(person, event)) {
            error = error + "Person is already registered to this event!";
        }
        error = error.trim();
    
        if (error.length() > 0) {
            throw new IllegalArgumentException(error);
        }
    
        Registration registration = new Registration();
    	registration.setId(person.getName().hashCode() * event.getName().hashCode());
        registration.setPerson(person);
        registration.setEvent(event);
    
        registrationRepository.save(registration);
    
        return registration;
    }
    
    @Transactional
    public List<Event> getEventsAttendedByPerson(Person person) {
        if (person == null ) {
            throw new IllegalArgumentException("Person cannot be null!");
        }
        List<Event> eventsAttendedByPerson = new ArrayList<>();
        for (Registration r : registrationRepository.findByPerson(person)) {
            eventsAttendedByPerson.add(r.getEvent());
        }
        return eventsAttendedByPerson;
    }
  7. Run the tests again, and all should be passing this time.

2.6.3. Service Integration Testing with the curl Tool

The command line utility curl is one way to automate integration testing for the REST API of your application. This brief section shows a basic examples for using it for testing with persons.

  1. Make sure you have a clean database for your integration tests. This can be done by using the spring.jpa.hibernate.ddl-auto=create-drop setting in the application.properties file for the test backend, or by exposing a database clear API function that is only used durint integration testing.

  2. Start the backend server.

  3. Issue curl -s http://localhost:8080/persons and observe the output!

  4. Use the -X switch to specify the used HTTP method:

    $ curl -s -X POST http://localhost:8080/persons/testperson1
    {"name":"testperson1","events":[]}
    $ curl -s -X POST http://localhost:8080/persons/testperson2
    {"name":"testperson2","events":[]}
    $ curl -s -X http://localhost:8080/persons
    [{"name":"testperson1","events":[]},{"name":"testperson2","events":[]}]
  5. To verify that a given content is in the returned values, you can use the standard output result of the command and filter it, for example, using grep

    $ curl -s -X GET  http://localhost:8080/persons | grep -o testperson1
    testperson1
  6. A way to get started with implementing a Gradle task for integration testing (expected in the second deliverable) is to call a command line tool (e.g., curl) from Gradle. Gradle documentation has a section on how to achieve this: https://docs.gradle.org/5.6.2/dsl/org.gradle.api.tasks.Exec.html#org.gradle.api.tasks.Exec

2.7. Assessing Code Coverage using EclEmma

This tutorial covers the basics of EclEmma and retrieves code coverage metrics using it.

2.7.1. Getting EclEmma

Install EclEmma as a plugin in your Eclipse IDE from here.

The Spring Tools Suite (STS) version of Eclipse already ships with the plugin pre-installed, so you can skip this step if you are using STS.

2.7.2. Example Gradle Project for Assessing Code Coverage

We will create a Gradle project from scratch and be testing a simple method returnAverage(int[], int, int, int) .

  1. Create a new Gradle project in Eclipse by clicking on File > New > Other
    New Project

  2. Under Gradle, choose Gradle Project
    New Gradle Project

  3. Click on Next, then name your project tutorial7, click on Finish
    Project Name

    The project may take some time to be created.
  4. Create a new package instead of the default ones for both the source and test folders (e.g ca.mcgill.ecse321.tutorial7) and move the default generated classes (Library and LibraryTest) to this package.
    Create Packages

  5. Change the code in the Library class

    package ca.mcgill.ecse321.tutorial7;
    
    public class Library {
    
    	public static double returnAverage(int value[], int arraySize, int MIN, int MAX) {
    		int index, ti, tv, sum;
    		double average;
    		index = 0;
    		ti = 0;
    		tv = 0;
    		sum = 0;
    		while (ti < arraySize && value[index] != -999) {
    			ti++;
    			if (value[index] >= MIN && value[index] <= MAX) {
    				tv++;
    				sum += value[index];
    			}
    			index++;
    		}
    		if (tv > 0)
    			average = (double) sum / tv;
    		else
    			average = (double) -999;
    		return average;
    	}
    }
  6. Change the code in the LibraryTest class

    package ca.mcgill.ecse321.tutorial7;
    
    import static org.junit.Assert.assertEquals;
    
    import org.junit.Test;
    
    public class LibraryTest {
    
    	@Test
    	public void allBranchCoverageMinimumTestCaseForReturnAverageTest1() {
    		int[] value = {5, 25, 15, -999};
    		int AS = 4;
    		int min = 10;
    		int max = 20;
    		double average = Library.returnAverage(value, AS, min, max);
    		assertEquals(15, average, 0.1);
    	}
    
    	@Test
    	public void allBranchCoverageMinimumTestCaseForReturnAverageTest2() {
    		int[] value = {};
    		int AS = 0;
    		int min = 10;
    		int max = 20;
    		double average = Library.returnAverage(value, AS, min, max);
    		assertEquals(-999.0, average, 0.1);
    	}
    }

2.7.3. Retrieving Code Coverage Metrics

We can straightforwardly manage code coverage using JaCoCo inside Eclipse with no configuration if we are using EclEmma Eclipse plugin.
  1. Run the Test in coverage mode using Eclemma. Click on LibraryTest, Coverage As, 1 JUnit Test
    Full Branch Coverage

  2. Verify that we have 100% branch coverage.
    Full Branch Coverage-Eclemma

2.8. Event Registration Application Unit Code Coverage

  1. Check the code coverage of the service unit tests in the EventRegistration-Backend project.

  2. If you want to run the tests using gradle, use the Jacoco plugin with plugin ID jacoco. After adding it to the build.gradle file, the plugin section should look like the one below:

plugins {
	id 'org.springframework.boot' version '2.2.4.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
	id 'jacoco'
}
  1. This new plugin gives us the jacocoTestReport task that can generate html reports. Try executing this task from the terminal and see the generated HTML files under build/reports/jacoco/test/html/!

  2. If you wish, you can check enforce a certain treshold on the test cases with this Jacoco plugin. With the definition below, the ./gradlew jacocoTestCoverageVerification task will fail if code coverage is below 60%.

jacocoTestCoverageVerification {
	violationRules {
		rule {
			limit {
				minimum = 0.6
			}
		}
	}
}

3. Frontend

3.1. Installation Instructions: Vue.js

Vue.js is a popular web frontend for building user interfacs in Javascript, which is considered to be easier to learn compared to React and Angular.

3.1.1. Install Vue.js

  1. Open a shell (or run cmd.exe in Windows)

  2. Check that you successfully installed node.js and npm e.g. by checking their versions:

    $ node -v
    v10.21.0 (or higher)
    $ npm -v
    6.14.4 (or higher)
  3. Install the command line interface (CLI) for Vue: sudo npm install --global vue-cli

3.1.2. Generate initial Vue.js project content

  1. Navigate to your local Git repository of the Event Registration System

    $ cd ~/git/eventregistration
  1. Generate initial content as follows

    • Hit Enter after each line if not indicated otherwise

    • Detailed instructions at https://github.com/vuejs-templates/webpack and https://bootstrap-vue.js.org/docs

      $ vue init bootstrap-vue/webpack eventregistration-frontend
      ? Project name (EventRegistration-Frontend) eventregistration-frontend
      ? Project description (A Vue.js project) A Vue.js frontend for Event Registration App
      ? Author (Your Name <your.email@provider.com>)
      ? Vue build (Use arrow keys):
      > Runtime + Compiler
        Runtime-only
      ? Install vue-router (Y/n): Y
      ? Use ESLint to lint your code (Y/n): n
      ? Setup unit tests with Karma + Mocha (Y/n) Y
      ? Setup e2e tests with Nightwatch (Y/n) Y
      
      vue-cli · Generated "eventregistration-frontend".
  2. Rename the generated directory to match the naming convention applied for the backend project

    mv eventregistration-frontend/ EventRegistration-Frontend
  3. Now execute the following commands (one after the other)

    $ cd EventRegistration-Frontend
    $ npm install
    $ npm run dev
  4. As a result A sample web page should appear at http://localhost:8080/

  5. You can stop this development server by pressing Ctrl+C in the shell

3.1.3. Install additional dependencies

  1. Install JQuery and Axios (we will use these dependencies for issuing REST API calls):

npm install --save jquery
npm install --save axios

3.1.4. Setting up your development server

  1. We change the default port to 8087 (instead of the default 8080) and the default IP address by using a configuration file. The rationale behind this step is that other Tomcat servers may already listen at the default localhost:8080 port which may clash with our development server.

  2. Open ./config/index.js and add port: 8087 to module.exports (both build and dev part)

    • The development server is set up at localhost, i.e. http://127.0.0.1:8087

    • The production server is set up in accordance with the virtual machines

    • We also store the host IP address and port of the backend server in similar environment variables (backendHost and backendPort).

      module.exports = {
        build: {
          env: require('./prod.env'),
          host: 'eventregistration-frontend-123.herokuapp.com',
          port: 443,
          backendHost: 'eventregistration-backend-123.herokuapp.com',
          backendPort: 443,
          //...
        },
        dev: {
          env: require('./dev.env'),
          host: '127.0.0.1',
          port: 8087,
          backendHost: '127.0.0.1',
          backendPort: 8080,
          //...
        }
      }
  3. Open ./build/dev-server.js, and change the uri assignment as follows:

    • The original line of code can be commented or deleted.

      //var uri = 'http://localhost:' + port
      var host = config.dev.host
      var uri = 'http://' + host + ':' + port
  4. Start again your development server by npm run dev. The same web application should now appear at http://127.0.0.1:8087/

  5. Stop the development server by pressing Ctrl+C.

3.1.5. Commit your work to Github

  1. If everything works then commit your work to your Github repository.

  2. Notice that many libraries and files are omitted, which is intentional. Check the .gitignore file for details.

3.2. Create a Static Vue.js Component

Vue.js promotes the use of components which encapsulate GUI elements and their behavior in order to build up rich user interfaces in a modular way. A component consists of

  • template: A template of (a part of) an HTML document enriched with data bindings, conditional expressions, loops, etc.

  • script: The behavior of the user interface programmed in JavaScript.

  • style: The customized graphical appearance of HTML document elements.

We will first create a new Vue.js component and then connect it to a backend Java Spring service via a Rest API call.

3.2.1. Create a component file

We use . below to refer to the EventRegistration-Frontend directory.
  1. Create a new file EventRegistration.vue in ./src/components with the following initial content:

    <template>
    </template>
    <script>
    </script>
    <style>
    </style>
  2. Create some static HTML content of the template part starting with a <div> element corresponding to your component. We

    <template>
      <div id="eventregistration">
        <h2>People</h2>
        <table>
          <tr>
              <td>John</td>
              <td>Event to attend</td>
          </tr>
          <tr>
              <td>
                  <input type="text" placeholder="Person Name">
              </td>
              <td>
                  <button>Create</button>
              </td>
          </tr>
        </table>
        <p>
          <span style="color:red">Error: Message text comes here</span>
        </p>
      </div>
    </template>
  3. Customize the <style> part with your designated CSS content. A detailed CSS reference documentation is available at https://www.w3schools.com/CSSref/. The final result of that part should like as follows.

    <style>
      #eventregistration {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        color: #2c3e50;
        background: #f2ece8;
      }
    </style>

3.2.2. Create a new routing command

  1. We need to route certain HTTP calls to a specific URL to be handled by EventRegistration.vue.

  2. Open ./src/router/index.js and add a new route by extending the existing routes property.

    export default new Router({
      routes: [
        {
          path: '/',
          name: 'Hello',
          component: Hello
        },
        {
          path: '/app',
          name: 'EventRegistration',
          component: EventRegistration
        }
      ]
    })
    • You should not change the number of spaces used as indentation otherwise you get error messages, if you have LInt enabled in your project.

    • Import the new component EventRegistration.vue at the beginning of ./src/router/index.js after all existing imports!

      // add import after all existing imports
      import EventRegistration from '@/components/EventRegistration'
  3. Start the development server and navigate your browser to http://127.0.0.1:8087/#/app. Your new Vue.js component should be rendered (with the static HTML content).

3.3. Vue.js Components with Dynamic Content

3.3.1. Add data and event handlers

Next we add event handling and dynamic content to our EventRegistration.vue component.

  1. Create another file registration.js in the same folder which will contain the Javascript code for the EventRegistration.vue component.

  2. Create constructor methods:

    function PersonDto (name) {
      this.name = name
      this.events = []
    }
    
    function EventDto (name, date, start, end) {
      this.name = name
      this.eventDate = date
      this.startTime = start
      this.endTime = end
    }
  3. Add data variables to the export declaration of the component.

    export default {
      name: 'eventregistration',
      data () {
        return {
          persons: [],
          newPerson: '',
          errorPerson: '',
          response: []
        }
      },
      //...
    }
  4. Add an initialization function below the data part.

    created: function () {
        // Test data
        const p1 = new PersonDto('John')
        const p2 = new PersonDto('Jill')
        // Sample initial content
        this.persons = [p1, p2]
      },
  5. Add event handling method createPerson():

    methods: {
        createPerson: function (personName) {
          // Create a new person and add it to the list of people
          var p = new PersonDto(personName)
          this.persons.push(p)
          // Reset the name field for new people
          this.newPerson = ''
        }
      }

3.3.2. Create dynamic data bindings

  1. Open EventRegistration.vue and link the Javascript file as script:

    <script src="./registration.js">
    </script>
  2. Change the static template content for the person list to dynamic bindings:

    • We iterate along all people in data property people and dynamically print their name by {{ person.name }} (see list rendering)

    • We print the (currently empty) list of events to which a person is registered to.

      <template>
        <div id="eventregistration">
          <h2>Persons</h2>
            <table>
              <tr v-for="person in persons" >
                  <td>{{ person.name }}</td>
                  <td>
                    <ul>
                      <li v-for="event in person.events">
                        {{event.name}}
                      </li>
                    </ul>
                  </td>
              </tr>
          <!-- ... -->
          </table>
        </div>
      </template>
  3. Link input field content with data variable newPerson and button clicks for Create Person for event handler method createPerson().

    <table>
      <!-- ... -->
    <tr>
        <td>
            <input type="text" v-model="newPerson" placeholder="Person Name">
        </td>
        <td>
            <button v-bind:disabled="!newPerson" @click="createPerson(newPerson)">Create Person</button>
        </td>
    </tr>
    </table>
  4. Bind the error message to the corresponding variable errorPerson by extending the <span> tag with conditional rendering.

    • The error message will only appear if the data property errorPerson is not empty.

    • You may wish to further refine error handling in case of empty string content for newPerson by adding && !newPerson to the condition.

      <span v-if="errorPerson" style="color:red">Error: {{errorPerson}} </span>
  5. Run your frontend application and observe that two people are listed.

3.4. Calling Backend Services

Next we change our frontend to issue calls to the backend via the Rest API provided by the Java Spring framework. Please refer to the section 3.6.2 where we enabled the Cross-Origin Resource Sharing at the controller level using '@CrossOrigin' notation.

3.4.1. Calling backend services in from Vue.js components

We need to modify our frontend to make calls to backend services.

  1. Open registration.js and add the following content to the beginning:

    • Note that instead of hard-wired IP addresses and ports, details are given in a configuration file.

      import axios from 'axios'
      var config = require('../../config')
      
      var frontendUrl = 'http://' + config.dev.host + ':' + config.dev.port
      var backendUrl = 'http://' + config.dev.backendHost + ':' + config.dev.backendPort
      
      var AXIOS = axios.create({
        baseURL: backendUrl,
        headers: { 'Access-Control-Allow-Origin': frontendUrl }
      })
  2. Now navigate to the created function, and replace existing content with the following lines:

      created: function () {
        // Initializing persons from backend
        AXIOS.get('/persons')
        .then(response => {
          // JSON responses are automatically parsed.
          this.persons = response.data
        })
        .catch(e => {
          this.errorPerson = e
        })
        // Initializing events
        AXIOS.get('/events')
        .then(response => {
          this.events = response.data
        })
        .catch(e => {
          this.errorEvent = e
          // this.errors.push(e)
        })
      }
  3. Navigate to the createPerson() method and change its content as follows:

    createPerson: function (personName) {
          AXIOS.post('/persons/'.concat(personName), {}, {})
            .then(response => {
            // JSON responses are automatically parsed.
              this.persons.push(response.data)
              this.errorPerson = ''
              this.newPerson = ''
            })
            .catch(e => {
              var errorMsg = e.response.data.message
              console.log(errorMsg)
              this.errorPerson = errorMsg
            })
        }
  4. Run the frontend application and check that

    • New people can be added

    • They immediately appear in the people list.

3.5. Build and Travis-CI

Travis-CI supports building nodejs projects. However, we do not want to run the default npm test command. Instead, the build should do npm install only.

3.6. Additional steps in the tutorial

3.6.1. Managing events

  1. List all events (name, eventDate, startTime, endTime)

    • Introduce an array events in the frontend data store

      Update your registration.js:

        data() {
          return {
            persons: [],
            events: [],
            // ... other data members
          }
        },
    • Call the appropriate backend service to fill the contents

      Update your registration.js:

        created: function () {
          // Initializing persons
          // See: was done above
      
          // Initializing events
          AXIOS.get('/events').then(response => {this.events = response.data}).catch(e => {this.errorEvent = e});
        },
    • Provide a dynamic list in the component and bind it to events

      Update your EventRegistration.vue:

          <span v-if="errorPerson" style="color:red">Error: {{errorPerson}}</span>
          <!-- This line above is the last line of the section we added in the previous section -- this is only here to ease the navigation in the code -->
      
      <hr>
        <h2>Events</h2>
        <table>
          <tr>
            <th>Event Name</th>
            <th>Date</th>
            <th>Start</th>
            <th>End</th>
            <!--<th>Edit</th>-->
          </tr>
          <tr v-for="event in events">
            <td>{{ event.name }}</td>
            <td>{{ event.eventDate }}</td>
            <td>{{ event.startTime }}</td>
            <td>{{ event.endTime }}</td>
            <!--<td>
              <button v-on:click="updateEvent(event.name)">Edit</button>
            </td>-->
          </tr>
        </table>
            <span v-if="errorEvent" style="color:red">Error: {{errorEvent}} </span>
      <hr>
  2. Create a new event (name, date, startTime, endTime)

    • Introduce an object newEvent in the frontend data store with four properties (e.g. name, date, startTime, endTime).

      • Set the initial values of these properties to somet value

        Update registration.js and add to data() at the top

              newEvent: {
                name: '',
                eventDate: '2017-12-08',
                startTime: '09:00',
                endTime: '11:00'
              },
              errorEvent: '',
    • Provide a button to initiate creating a new event and provide HTML input fields to set event details

    • Create a call to the appropriate backend service, i.e. createEvent()

      Update EventRegistration.vue

            <!-- Add this to the bottom of the table created for displaying events -->
        <tr>
            <td>
              <input type="text" v-model="newEvent.name" placeholder="Event Name">
            </td>
            <td>
              <input type="date" v-model="newEvent.eventDate" placeholder="YYYY-MM-DD">
            </td>
            <td>
              <input type="time" v-model="newEvent.startTime" placeholder="HH:mm">
            </td>
            <td>
              <input type="time" v-model="newEvent.endTime" placeholder="HH:mm">
            </td>
            <td>
              <button
                <button v-bind:disabled="!newEvent.name" v-on:click="createEvent(newEvent.name, newEvent.eventDate, newEvent.startTime, newEvent.endTime)">Create</button>
            </td>
        </tr>
    • Introduce an object errorEvent for error message related to event creation

      This one has been done for registration.js already (errorEvent in data())

    • Provide corresponding HTML field for displaying the error message (e.g. <span>), and set its appearance condition to the content of the error message

      Update EventRegistration.vue: add the following code to the event table

          <span v-if="errorEvent" style="color:red">Error: {{errorEvent}} </span>
  3. Register a person to an event (when a new event should occur in the list of events printed next to a person)

    • Provide a selection of people

      • You need a corresponding data variable (e.g. selectedPerson)

      • You can use the HTML <select v-model="selectedPerson"> tag where each option (<option> tag with v-for Vue.js parameter) is filled dynamically from the list of people.

      • Hint: You can add a first disabled option as follows:

        <option disabled value="">Please select one</option>
    • Provide a selection of events in a similar way.

    • Provide a button to initiate registration

    • Enable the button only if both a person and an event are selected

      The solution for the above bullet points (goes to EventRegistration.vue):

        <hr>
          <h2>Registrations</h2>
          <label>Person:
            <select v-model="selectedPerson">
              <option disabled value="">Please select one</option>
              <option v-for="person in persons" >
                {{ person.name }}
              </option>
            </select>
          </label>
          <label>Event:
            <select v-model="selectedEvent">
              <option disabled value="">Please select one</option>
              <option v-for="event in events" >
                {{ event.name }}
              </option>
            </select>
          </label>
          <button v-bind:disabled="!selectedPerson || !selectedEvent" @click="registerEvent(selectedPerson,selectedEvent)">Register</button>
          <hr>
    • Implement the register method in registration.js:

              registerEvent: function (personName, eventName) {
            var indexEv = this.events.map(x => x.name).indexOf(eventName)
            var indexPart = this.persons.map(x => x.name).indexOf(personName)
            var person = this.persons[indexPart]
            var event = this.events[indexEv]
            AXIOS.post('/register', {},
              {params: {
                person: person.name,
                event: event.name}})
            .then(response => {
              // Update appropriate DTO collections
              person.events.push(event)
              this.selectedPerson = ''
              this.selectedEvent = ''
              this.errorRegistration = ''
            })
            .catch(e => {
              var errorMsg = e
              console.log(errorMsg)
              this.errorRegistration = errorMsg
            })
          },
      1. To run your applicaiton, use npm install and npm run dev

      2. See https://github.com/Rijul5/eventregistration3 for the completed solution

4. Android

4.1. Setting up your repository

  1. Navigate to the directory where the EventRegistration application resides, e.g., /home/user/git/eventregistration

  2. Initialize a new, orphan branch with the following commands

git checkout --orphan android
git reset
rm -rf ./* .gitignore
One-liner command that does the same for those who like living dangerously:
git checkout --orphan android && git reset && rm -rf ./* .gitignore

4.2. Create an Android project

  1. Start a new Android project Start new Android project

  2. Select a Basic Activity and click Next

    Select basic activity

  3. Specify project details and click on Finish

    • Application name: EventRegistration-Android

    • Package name: ca.mcgill.ecse321.eventregistration

    • Project location: create the project within the prepared working copy of the git repository say, /home/user/git/eventregistration/EventRegistration-Android`)

    • Select Java as language

    • Click on Finish

      Target platform settings

  4. Wait until the project is built by Gradle, this takes a minute or two

  5. Optional step.

    Optionally, to setup version control integration, go to File/Settings…​/Version Control and add the repository as Git root to the project settings
    Set Git root location
    Then, you can issue Git commands from the VCS menu in Android Studio while developing the application. Regardless whether you complete this step or not, you can still use git from the command line, since the project is created in the working directory of your git repository.
  6. Select the Project view in the left pane (instead of the default Android view) and observe three files:

    Project view

    • MainActivity.java: application code is written here (located in app/src/main/java)

    • content_main.xml: layout specifications of the UI are provided in XML (located in app/src/main/res/layout)

    • strings.xml: naming of resources (located in app/src/main/res/values)

  7. Include a dependency for network communication by adding the following line to the build.gradle file located in the app folder to the end within the dependencies{ …​ } part (see figure, but the content is different).

        implementation 'com.loopj.android:android-async-http:1.4.9'
  8. Open the AndroidManifest.xml file (located in app/src/main within the Android project), and add the following XML tag for setting permissions appropriately (before the existing <application> tag)

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="ca.mcgill.ecse321.eventregistration">
    
        <uses-permission android:name="android.permission.INTERNET"/>
        <!-- Existing content with <application> tag -->
    </manifest>
  9. As the gradle build file has changed, click on the Sync link.

  10. Re-build the project by Build | Make Project if still needed.

4.3. Developing for Android: Part 1

4.3.1. Developing the View Layout

In the next steps, we will develop a simple GUI as the view for the mobile EventRegistration app with (1) one text field for specifying the name of a person, and (2) one Add Person button

The GUI will look like as depicted below.
Simple GUI

  1. Open the content_main.xml file, which contains a default Hello World text.

  2. Replace the highlighted default content with the following XML tags.
    Android Hello World

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/error"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:visibility="gone"
            android:text=""
            android:textColor="#FF0000"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/newperson_name"
            android:hint="@string/newperson_hint"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:text="@string/newperson_button"
            android:onClick="addPerson"/>
    </LinearLayout>
  • LinearLayout declares a vertical layout to hold the GUI elements;

  • EditText adds a textfield to enter the name of the person;

  • Button provides a button to add a person.

    Some erroneous tags are marked in red, which will be corrected in the following steps.

Specifying a text field and a button
  1. We place new literals in the strings.xml

    <string name="newperson_hint">Who?</string>
    <string name="newperson_button">Add Person</string>
  2. Save strings.xml

Observing the view
  1. Save the file content_main.xml.

  2. Click on the Design tab to check the graphical preview of the app.

    Graphical content view

4.3.2. Connecting to backend via RESTful service calls

As a next step, we define a view depicted below and add Java code to provide behavior for the view, e.g. what should happen when the different buttons are clicked. The key interactions of our application are the following:

  1. What to do when the application is launched? (onCreate())

  2. What to do when a button is clicked? (addPerson())

Create a utility class for communicating with HTTP messages
  1. Make sure you have the implementation 'com.loopj.android:android-async-http:1.4.9' dependency (among others) in the build.gradle file for the app module (see the section on project setup for more details)

  2. Create the HttpUtils class in the ca.mcgill.ecse321.eventregistration package and add missing imports as required with Alt+Enter

    You may need to wait a few minutes after dependencies have been resolved to allow the IDE to index classes
    public class HttpUtils {
        public static final String DEFAULT_BASE_URL = "https://eventregistration-backend-123.herokuapp.com/";
    
        private static String baseUrl;
        private static AsyncHttpClient client = new AsyncHttpClient();
    
        static {
            baseUrl = DEFAULT_BASE_URL;
        }
    
        public static String getBaseUrl() {
            return baseUrl;
        }
    
        public static void setBaseUrl(String baseUrl) {
            HttpUtils.baseUrl = baseUrl;
        }
    
        public static void get(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
            client.get(getAbsoluteUrl(url), params, responseHandler);
        }
    
        public static void post(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
            client.post(getAbsoluteUrl(url), params, responseHandler);
        }
    
        public static void getByUrl(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
            client.get(url, params, responseHandler);
        }
    
        public static void postByUrl(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
            client.post(url, params, responseHandler);
        }
    
        private static String getAbsoluteUrl(String relativeUrl) {
            return baseUrl + relativeUrl;
        }
    }
Further helper methods
  1. Open the MainActivity.java file.

  2. Add a new attribute to the beginning of the class for error handling.

    // ...
    public class MainActivity extends AppCompatActivity {
      private String error = null;
    
      // ...
    }
  3. Implement the refreshErrorMessage() method to display the error message on the screen, if there is any.

    Again, add imports with Alt+Enter (import is needed for TextView)
    private void refreshErrorMessage() {
      // set the error message
      TextView tvError = (TextView) findViewById(R.id.error);
      tvError.setText(error);
    
      if (error == null || error.length() == 0) {
        tvError.setVisibility(View.GONE);
      } else {
        tvError.setVisibility(View.VISIBLE);
      }
    }
  4. Add code to initialize the application in the onCreate() method (after the auto-generated code).

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      // ...
      // INSERT TO END OF THE METHOD AFTER AUTO-GENERATED CODE
      // initialize error message text view
      refreshErrorMessage();
    }
Creating a handler for Add Person button
  1. Implement the addPerson() method as follows

    public void addPerson(View v) {
      error = "";
      final TextView tv = (TextView) findViewById(R.id.newperson_name);
      HttpUtils.post("persons/" + tv.getText().toString(), new RequestParams(), new JsonHttpResponseHandler() {
          @Override
          public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
              refreshErrorMessage();
              tv.setText("");
          }
          @Override
          public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
              try {
                  error += errorResponse.get("message").toString();
              } catch (JSONException e) {
                  error += e.getMessage();
              }
              refreshErrorMessage();
          }
      });
    }
  2. Import the missing classes again with Alt+Enter. There are multiple Header classes available, you need to import the cz.msebera.android.httpclient.Header class.

4.4. Running and Testing the Application on a Virtual Device

  1. Start the Spring backend application on Heroku, while ensure that the DEFAULT_BASE_URL in HttpUtils is configured accordingly.

  2. Click on the AVD manager button in Android Studio near the top right corner of the window

    Open terminal

  3. Add a new device with default settings.

    You might be asked to download emulator files. If this happens, click OK.
  4. Start/Debug the application using the buttons highlighted on the figure below

    Select system image

  5. After you select the device (in this case Nexus 5X) as deployment target, the application will automatically be deployed and started within an Android VM, and it is ready to use. Be patient, deployment and startup may take a few seconds.

  6. Supply a name for a new person, then try adding it. Upon successful completion, the text field and the error message should clear.

  7. Supply the same name in the text field and then try adding the person. You should get an error message on screen "Person has already been created".

4.5. Developing for Android (Part 2)

You can use the https://eventregistration-backend-123.herokuapp.com backend URL for the event registration example in the HttpUtils class.

As a next step, we extend the view and its behavior. Key interactions of our application added in this phase are the following:

  1. What to do when the application is launched? (onCreate())

  2. What to do when application data is updated? (refreshLists())

  3. What to do when a button is clicked? (addEvent(), and register())

The expected layout of the application:

android studio main layout

4.5.1. Create helper classes

  1. Create the classes included in the next two steps within the ca.mcgill.ecse321.eventregistration package

  2. Create a new class called DatePickerFragment

    Import missing classes with Alt+Enter. If the import option offers multiple classes, choose the ones that are not deprecated.
    public class DatePickerFragment extends DialogFragment
            implements DatePickerDialog.OnDateSetListener {
    
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            // Use the current date as the default date in the picker
            final Calendar c = Calendar.getInstance();
            int year = c.get(Calendar.YEAR);
            int month = c.get(Calendar.MONTH);
            int day = c.get(Calendar.DAY_OF_MONTH);
    
            // Parse the existing time from the arguments
            Bundle args = getArguments();
            if (args != null) {
                year = args.getInt("year");
                month = args.getInt("month");
                day = args.getInt("day");
            }
    
            // Create a new instance of DatePickerDialog and return it
            return new DatePickerDialog(getActivity(), this, year, month, day);
        }
    
        public void onDateSet(DatePicker view, int year, int month, int day) {
            MainActivity myActivity = (MainActivity)getActivity();
            myActivity.setDate(getArguments().getInt("id"), day, month, year);
        }
    }
  3. Create a new class called TimePickerFragment

    The method setTime is missing from the MainActivity class and will be added later (i.e., the error message which complains that this method is missing is normal)
    public class TimePickerFragment extends DialogFragment
            implements TimePickerDialog.OnTimeSetListener {
    
        String label;
    
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            int hour = 0;
            int minute = 0;
    
            // Parse the existing time from the arguments
            Bundle args = getArguments();
            if (args != null) {
                hour = args.getInt("hour");
                minute = args.getInt("minute");
            }
    
            // Create a new instance of TimePickerDialog and return it
            return new TimePickerDialog(getActivity(), this, hour, minute,
                    DateFormat.is24HourFormat(getActivity()));
        }
    
        public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
            MainActivity myActivity = (MainActivity)getActivity();
            myActivity.setTime(getArguments().getInt("id"), hourOfDay, minute);
        }
    }
  4. Add the following helper methods within the MainActivity class to support date and time pickers

    private Bundle getTimeFromLabel(String text) {
        Bundle rtn = new Bundle();
        String comps[] = text.toString().split(":");
        int hour = 12;
        int minute = 0;
    
        if (comps.length == 2) {
            hour = Integer.parseInt(comps[0]);
            minute = Integer.parseInt(comps[1]);
        }
    
        rtn.putInt("hour", hour);
        rtn.putInt("minute", minute);
    
        return rtn;
    }
    
    private Bundle getDateFromLabel(String text) {
        Bundle rtn = new Bundle();
        String comps[] = text.toString().split("-");
        int day = 1;
        int month = 1;
        int year = 1;
    
        if (comps.length == 3) {
            day = Integer.parseInt(comps[0]);
            month = Integer.parseInt(comps[1]);
            year = Integer.parseInt(comps[2]);
        }
    
        rtn.putInt("day", day);
        rtn.putInt("month", month-1);
        rtn.putInt("year", year);
    
        return rtn;
    }
    
    public void showTimePickerDialog(View v) {
        TextView tf = (TextView) v;
        Bundle args = getTimeFromLabel(tf.getText().toString());
        args.putInt("id", v.getId());
    
        TimePickerFragment newFragment = new TimePickerFragment();
        newFragment.setArguments(args);
        newFragment.show(getSupportFragmentManager(), "timePicker");
    }
    
    public void showDatePickerDialog(View v) {
        TextView tf = (TextView) v;
        Bundle args = getDateFromLabel(tf.getText().toString());
        args.putInt("id", v.getId());
    
        DatePickerFragment newFragment = new DatePickerFragment();
        newFragment.setArguments(args);
        newFragment.show(getSupportFragmentManager(), "datePicker");
    }
    
    public void setTime(int id, int h, int m) {
        TextView tv = (TextView) findViewById(id);
        tv.setText(String.format("%02d:%02d", h, m));
    }
    
    public void setDate(int id, int d, int m, int y) {
        TextView tv = (TextView) findViewById(id);
        tv.setText(String.format("%02d-%02d-%04d", d, m + 1, y));
    }

4.5.2. Update view definition

  1. The corresponding complete view definition in the content_main.xml file is the following. We are using <RelativeLayout> now:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/content_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context="ca.mcgill.ecse321.eventregistration.MainActivity"
        tools:showIn="@layout/activity_main">
    
    
        <LinearLayout
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:orientation="vertical">
            <TextView
                android:id="@+id/error"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:visibility="gone"
                android:text=""
                android:textColor="#FF0000"/>
    
            <LinearLayout
                android:layout_height="wrap_content"
                android:layout_width="match_parent"
                android:orientation="vertical">
                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:orientation="vertical">
                    <LinearLayout
                        android:orientation="horizontal"
                        android:layout_height="wrap_content"
                        android:layout_width="match_parent">
    
                        <TextView
                            android:layout_height="wrap_content"
                            android:layout_width="wrap_content"
                            android:text="@string/personspinner_label"/>
    
                        <Spinner
                            android:layout_height="wrap_content"
                            android:layout_width="wrap_content"
                            android:layout_gravity="end"
                            android:prompt="@string/name_prompt"
                            android:id="@+id/personspinner"/>
    
                    </LinearLayout>
    
                    <LinearLayout
                        android:orientation="horizontal"
                        android:layout_height="wrap_content"
                        android:layout_width="match_parent">
    
                        <TextView
                            android:layout_height="wrap_content"
                            android:layout_width="wrap_content"
                            android:text="@string/eventspinner_label"/>
    
                        <Spinner
                            android:id="@+id/eventspinner"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_gravity="end"
                            android:prompt="@string/name_prompt"
                            android:layout_margin="0dp"/>
    
                    </LinearLayout>
    
                </LinearLayout>
                <LinearLayout
                    android:orientation="horizontal"
                    android:layout_height="wrap_content"
                    android:layout_width="match_parent">
                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:text="@string/register_button"
                        android:onClick="register"
                        android:layout_gravity="start"/>
                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:text="@string/refresh_button"
                        android:onClick="refreshLists"
                        android:layout_gravity="end"/>
                </LinearLayout>
            </LinearLayout>
    
    
            <View
                android:layout_height="2dp"
                android:layout_width="fill_parent"
                android:background="#16552e"/>
    
            <LinearLayout
                android:orientation="vertical"
                android:layout_height="wrap_content"
                android:layout_width="match_parent">
                <EditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:id="@+id/newperson_name"
                    android:hint="@string/newperson_hint"/>
                <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:text="@string/newperson_button"
                    android:onClick="addPerson"/>
            </LinearLayout>
    
            <View
                android:layout_height="2dp"
                android:layout_width="fill_parent"
                android:background="#16552e"/>
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <EditText android:id="@+id/newevent_name"
                    android:layout_height="wrap_content"
                    android:layout_width="fill_parent"
                    android:hint="@string/newevent_hint"/>
                <LinearLayout
                    android:orientation="horizontal"
                    android:layout_height="wrap_content"
                    android:layout_width="match_parent">
                    <TextView
                        android:layout_height="wrap_content"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:text="@string/newevent_date_label"/>
                    <TextView
                        android:layout_height="wrap_content"
                        android:layout_width="wrap_content"
                        android:text="@string/newevent_date_first"
                        android:layout_gravity="end"
                        android:id="@+id/newevent_date"
                        android:onClick="showDatePickerDialog"/>
                </LinearLayout>
    
                <LinearLayout
                    android:orientation="horizontal"
                    android:layout_height="wrap_content"
                    android:layout_width="match_parent">
                    <TextView
                        android:layout_height="wrap_content"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:text="@string/starttime_label"/>
                    <TextView
                        android:layout_height="wrap_content"
                        android:layout_width="wrap_content"
                        android:text="@string/starttime_first"
                        android:layout_gravity="end"
                        android:id="@+id/starttime"
                        android:onClick="showTimePickerDialog"/>
                </LinearLayout>
    
                <LinearLayout
                    android:orientation="horizontal"
                    android:layout_height="wrap_content"
                    android:layout_width="match_parent">
                    <TextView
                        android:layout_height="wrap_content"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:text="@string/endtime_label"/>
                    <TextView
                        android:layout_height="wrap_content"
                        android:layout_width="wrap_content"
                        android:text="@string/endtime_first"
                        android:layout_gravity="end"
                        android:id="@+id/endtime"
                        android:onClick="showTimePickerDialog"/>
                </LinearLayout>
                <Button
                    android:layout_height="wrap_content"
                    android:layout_width="wrap_content"
                    android:text="@string/newevent_button"
                    android:layout_gravity="end"
                    android:onClick="addEvent"/>
            </LinearLayout>
        </LinearLayout>
    
    </RelativeLayout>
  2. The complete string definitions go in the res/values/strings.xml resource

    <resources>
        <string name="app_name">Event Registration</string>
        <string name="action_settings">Settings</string>
        <string name="newperson_hint">Who?</string>
        <string name="newperson_button">Add Person</string>
        <string name="newevent_date_label">Date?</string>
        <string name="personspinner_label">Person?</string>
        <string name="eventspinner_label">Event?</string>
        <string name="starttime_label">Start time?</string>
        <string name="endtime_label">End time?</string>
        <string name="newevent_date_first">01-01-2017</string>
        <string name="newevent_button">Add Event</string>
        <string name="starttime_first">10:00</string>
        <string name="endtime_first">11:00</string>
        <string name="register_button">Register</string>
        <string name="newevent_hint">Event name?</string>
        <string name="refresh_button">Refresh lists</string>
        <string name="name_prompt">Select name</string>
    </resources>
    • TODO: add a Register button to allow registering a selected person to a selected event (call the register() method when clicked - this is to be implemented in the upcoming steps)

    • TODO: add a Refresh Lists button that refreshes the contents of the event and person spinners (call the refreshLists() method when clicked)

    • TODO: add a label with text End? below the Start? label

    • TODO: add a time picker to select the end time of a new event

    • TODO: add an Add Event button to allow creating new events from the user interface (call the addEvent() method when clicked - this is to be implemented in the upcoming steps)

4.5.3. Initialization on application launch

  1. Open the MainActivity.java file.

  2. Add a few new attributes to the beginning of the class as helpers for persistence and error handling.

    public class MainActivity extends AppCompatActivity {
      private String error = null;
      // APPEND NEW CONTENT STARTING FROM HERE
      private List<String> personNames = new ArrayList<>();
      private ArrayAdapter<String> personAdapter;
      private List<String> eventNames = new ArrayList<>();
      private ArrayAdapter<String> eventAdapter;
    
      //...
    }
    • Import missing classes (e.g. use Alt+Enter)

  3. Add code to initialize the application with data from the server in the onCreate() method (after the auto-generated code).

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      // ...
      // INSERT TO END OF THE METHOD
      // Add adapters to spinner lists and refresh spinner content
      Spinner personSpinner = (Spinner) findViewById(R.id.personspinner);
      Spinner eventSpinner = (Spinner) findViewById(R.id.eventspinner);
    
      personAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, personNames);
      personAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
      personSpinner.setAdapter(personAdapter);
    
      eventAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, eventNames);
      eventAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
      eventSpinner.setAdapter(eventAdapter);
    
      // Get initial content for spinners
      refreshLists(this.getCurrentFocus());
    }

At this point the refreshLists() method is missing, this is to be implemented in the upcoming steps.

4.5.4. Reactions to updated data

  1. Create the missing new method refreshLists() which seeks for the event and person spinners and sets their content according to the data retrieved from the server

    public void refreshLists(View view) {
        refreshList(personAdapter ,personNames, "people");
        refreshList(eventAdapter, eventNames, "events");
    }
    
    private void refreshList(final ArrayAdapter<String> adapter, final List<String> names, final String restFunctionName) {
        HttpUtils.get(restFunctionName, new RequestParams(), new JsonHttpResponseHandler() {
    
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONArray response) {
                names.clear();
                names.add("Please select...");
                for( int i = 0; i < response.length(); i++){
                    try {
                        names.add(response.getJSONObject(i).getString("name"));
                    } catch (Exception e) {
                        error += e.getMessage();
                    }
                    refreshErrorMessage();
                }
                adapter.notifyDataSetChanged();
            }
    
            @Override
            public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
                try {
                    error += errorResponse.get("message").toString();
                } catch (JSONException e) {
                    error += e.getMessage();
                }
                refreshErrorMessage();
            }
        });
    }
  2. Implement the addEvent() method

    public void addEvent(View v) {
        // start time
        TextView tv = (TextView) findViewById(R.id.starttime);
        String text = tv.getText().toString();
        String comps[] = text.split(":");
    
        int startHours = Integer.parseInt(comps[0]);
        int startMinutes = Integer.parseInt(comps[1]);
    
        // TODO get end time
    
        // date
        tv = (TextView) findViewById(R.id.newevent_date);
        text = tv.getText().toString();
        comps = text.split("-");
    
        int year = Integer.parseInt(comps[2]);
        int month = Integer.parseInt(comps[1]);
        int day = Integer.parseInt(comps[0]);
    
        // name
        tv = (TextView) findViewById(R.id.newevent_name);
        String name = tv.getText().toString();
    
        // Reminder: calling the service looks like this:
        // https://eventregistration-backend-123.herokuapp.com/events/testEvent?date=2013-10-23&startTime=00:00&endTime=23:59
    
        RequestParams rp = new RequestParams();
    
        NumberFormat formatter = new DecimalFormat("00");
        rp.add("date", year + "-" + formatter.format(month) + "-" + formatter.format(day));
        rp.add("startTime", formatter.format(startHours) + ":" + formatter.format(startMinutes));
        // TODO add end time as parameter
    
        HttpUtils.post("events/" + name, rp, new JsonHttpResponseHandler() {
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
                refreshErrorMessage();
                ((TextView) findViewById(R.id.newevent_name)).setText("");
            }
    
            @Override
            public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
                try {
                    error += errorResponse.get("message").toString();
                } catch (JSONException e) {
                    error += e.getMessage();
                }
                refreshErrorMessage();
            }
        });
    }
    • TODO: get the end time of the new event

    • TODO: supply the end time to the REST request as an additional parameter

  3. Implement the register() method

    public void register(View v) {
    
        Spinner partSpinner = (Spinner) findViewById(R.id.personspinner);
        Spinner eventSpinner = (Spinner) findViewById(R.id.eventspinner);
    
        error = "";
    
        // TODO issue an HTTP POST here
        // Reminder: calling the service looks like this:
        // https://eventregistration-backend-123.herokuapp.com/register?person=testPreson&event=testEvent
    
    
        // Set back the spinners to the initial state after posting the request
        partSpinner.setSelection(0);
        eventSpinner.setSelection(0);
    
        refreshErrorMessage();
    }
    • TODO: implement the HTTP POST part of the register() method on your own

  4. See https://github.com/Rijul5/eventregistration3/tree/android for the complete solution of mobile frontend.