Call/WhatsApp/Text: +44 20 3289 5183

Question: Build a REST web application using the Spring Boot framework with a report documenting the build process

28 Feb 2024,7:24 PM

 

Build a REST web application using the Spring Boot framework with a report documenting the build process.

 

You are required to develop a Spring Boot based web application similar to that which was developed in the lab exercises. You should start by creating a base project using the online Spring Initializr. You should use the provided generic-interface.html JavaScript interface to develop and demonstrate your application (your application must serve this as a static web page for it to work properly). The application should use JPA persistence with the H2 in-memory database for development purposes. It should offer the following API to clients:

Method

URI

Body

Operation

GET

api/modules

none

return a JSON document containing a list of names and URIs for all module entries in the system

GET

api/modules/{module}

none

return a JSON document containing a list of names and URIs for all students for the specified module

GET

api/modules/{module}/students/all

none

return a JSON document containing a result code, and a list of all students for the specified module if the operation was successful

GET

api/modules/{module}/students/{id}

none

return a JSON document containing a result code, and the specified student for the specified module if the operation was successful

POST

api/modules/{module}/students

JSON description of a new student item

stores the student for the module and returns a JSON document containing a result code, and a URI suitable to get the newly stored student if the operation was successful

PUT

api/modules/{module}/{id}

JSON description of a new student item

replaces the given student and return a JSON document containing a result code, and a URI suitable to get the newly stored student if the operation was successful

DELETE

api/modules/{module}/{id}

none

deletes the given student and returns a JSON document containing a result code

URI elements enclosed in { } represent places where appropriate values would be substituted.

Each student must have the following fields, described here as TYPE followed by name.

LONG id, STRING name, STRING email, DOUBLE grade, LONG aeLevel, STRING fiCycle

You are strongly advised to make the module value for a student a field in the student class. While it might seem more natural to have separate collections for each value of module this will greatly increase the complexity of what you need to do. Using a field allows a single collection of objects to be persisted, read and written. The "result codes" referred to in the table above may take any form but should indicate either successful completion or an error condition.

 

 

01-concurrency and threads

Lab Exercises

BooYah

  1. Create the BooYah example from the lecture slides but change both the booloop and yahloop to only run 299 times - this just means the program runs for shorter and produces less lines of output.
  2. Run the BooYah example make sure you understand its output.
  3. Add a delay to both the booloop() and yahloop() methods by inserting the following lines after the print statement, but inside the loop:

try { Thread.sleep((long)(Math.random() * 100)); } catch(InterruptedException x) {}

 

Run the program to see how the interleaving of the threads calls varies.

Now make these changes:

  1. Change the yahloop() method so that it takes a parameter of type Thread.
  2. Fix the error in the main method where yahloop() is called without an argument, by passing in the variable t0.
  3. Add the following line to the yahloop() method immediately after the delay line you added earlier:

if (i == 200) { t0.interrupt(); }

 

  1. Now change the booloop() method, so that the delay line is replaced by this code (which includes the delay line too):

      if (i == 100) {

        try {

          Thread.sleep(1000*60*10);

        } catch(InterruptedException x) {

          System.out.println("BOO! I WAS INTERRUPTED!");

        }

      } else {

        try { Thread.sleep((long) (Math.random() * 10)); } catch(InterruptedException x) {}

      }

 

What we have done is this. When the booloop has looped 100 times, the thread will go to sleep for 600 seconds (ten minutes). When that happens you will see "Boo!" stops getting printed and only "Yah!" is being printed, because the boo thread is asleep. However, before ten minutes has passed, the yah loop will reach 200 iterations, and when it does it will call the interrupt() method on the variable that references the boo thread. This will cause the boo thread to be woken up and immediately throw an InterruptedException. The exception is caught and a message is printed, and then the boo thread resumes its loop.

  1. Run the program to see this happening and make sure you understand it.
  2. Add this method to your program:

  public static void dojoin(Thread t) {

    System.out.println("Waiting to join " + t);

    try { t.join(); } catch(InterruptedException x) {}

    System.out.println("Oh, at last!");

  }

 

  1. Add the following code to the main method, replacing the line yahloop(t0); with these three lines instead (of which the middle one is the same):

    new Thread(()->dojoin(t0)).start();

    yahloop(t0);

    System.out.println("Main thread exiting");

 

  1. Run the program again and examine all the output carefully and make sure you understand what you see. You may need to refer to the lecture notes and ask your tutor to fully understand this!

Counter class

  1. Create the first version of the Counter class in the lecture slides, and the CounterDemo program to go with it.
  2. Run CounterDemo and observe the results - you should see lost updates occurring.
  3. Refer back to the slides and make sure you understand what is happening and why.
  4. Fix the Counter class by adding synchronization as described in the lecture slides and verify that there are no longer any lost updates.

Deadlock

  1. Create a new class called Deadlock as shown below:

public class Deadlock implements Runnable {

 

    private Object o1 = "o1";

    private Object o2 = "o2";

 

    public void method1() {

      String name = Thread.currentThread().getName();

      System.out.println(name + " entered method 1 - attempting to obtain lock o1");

      synchronized(o1) {

        System.out.println(name + " obtained lock o1 in method 1 - attempting to obtain lock o2");

        try { Thread.sleep((long) (Math.random() * 100)); } catch( InterruptedException x) {}

        synchronized(o2) {

          System.out.println(name + " holding both locks in method 1");

        }

        System.out.println(name + " released o2 still holding o1 in method 1");

      }

      System.out.println(name + " exiting method 1 - no locks held");

    }

 

    public void method2() {

      String name = Thread.currentThread().getName();

      System.out.println(name + " entered method 2 - attempting to obtain lock o2");

      synchronized(o2) {

        System.out.println(name + " obtained lock o2 in method 2 - attempting to obtain lock o1");

        try { Thread.sleep((long) (Math.random() * 100)); } catch( InterruptedException x) {}

        synchronized(o1) {

          System.out.println(name + " holding both locks in method 2");

        }

        System.out.println(name + " released o1 still holding o2 in method 2");

      }

      System.out.println(name + " exiting method 2 - no locks held");

    }

   

    public void run() {

      String name = Thread.currentThread().getName();

      for(int i = 0; i < 10; i++) {

        try { Thread.sleep((long) (Math.random() * 100)); } catch( InterruptedException x) {}

        method1();

        try { Thread.sleep((long) (Math.random() * 100)); } catch( InterruptedException x) {}

        method2();

        System.out.println(name + " completed loop " + i);       

      }

      System.out.println(name + " successfully completed!");       

    }

 

    public static void main(String[] args) {

      Deadlock dl = new Deadlock();

      Thread t0 = new Thread(dl);

      Thread t1 = new Thread(dl);

      t0.start();

      t1.start();

    }

  }

This code is based on the deadlock example from the lecture slides.

  1. Inspect the code and run the program - you should see it lock up in a deadlock state (it is possible, though highly unlikely, that it could run to completion).
  2. Trace the print statements to understand how and where the deadlock occurred.
  3. Run the program a few more times to see variations of the thread interleaving that results in a deadlock condition.
  4. The lecture slides give two ways to fix the problem in this program; try both and make sure that the program then runs to completion consistently.

 

 

 

02-dispatch queues

Lab Exercises

  1. GUI

Work through the lecture slides that demonstrate the various versions of the little GUI program that has a button that displays "ouch" 100 times. The various versions are available on the VLE with these exercises - you should run each of them as you read through the corresponding lecture slides and experiment with resizing the UI while they are running to see how the UI responsiveness is affected in each case.

  1. FileLister

Download the FileLister class from the VLE and add it to a new Java project, ensuring you create the correct package to match the file’s declaration.

FileLister is a simple GUI based application that lists files from the Hard Drive and displays them in a list component. This program is strongly analogous to the GUI example in the lecture; instead of printing “ouch!” the button obtains a list of files; instead of displaying the output on a JLabel, the files are added to a UI list component. In its initial state, the work of listing the files is executed directly on the AWT event dispatch thread. This means no update to the UI can be made until it has completed, and since it lists all files recursively in C:\Program Files it takes a long time.

  1. Run the program in its initial state and observe its behaviour.
  2. Modify the program so that the file listing takes place on a new dedicated thread. Observe that the updating of the UI is not correct if this separate thread directly calls appendFile(File).
  3. Modify the program so that it makes its updates by queuing jobs on the AWT thread so that everything works as expected.

This is basically following the steps described in the lecture for the GUI example, but in this program.

3. Simulating concurrency with a dispatch queue

  1. Create the CountingJob and NaiveDispatcher classes as shown below in your IDE.

public class CountingJob implements Runnable {

 

  private final String jobName;

  private final int start;

  private final int length;

     

  public CountingJob(String jobName, int start, int length) {

    this.jobName = jobName;

    this.start = start;

    this.length = length;

  }

 

  @Override

  public void run() {

    NaiveDispatcher.printNumbers(jobName, start, length);

  }

 

}

 

 

import java.util.LinkedList;

import java.util.Queue;

 

public class NaiveDispatcher implements Runnable {

  private final Queue<Runnable> queue = new LinkedList<Runnable>();

 

  public void queueRunnable( Runnable toQueue) {

    this.queue.add( toQueue);

  }

 

  public void run() {

    while(true) {

      Runnable next = this.queue.poll();

      if ( next != null) {

        next.run();

      }

    }

  }

 

  public static void printNumbers(String jobName, int start, int length) {

    for (int i = start; i < start + length; i++) {

      System.out.println(jobName + " " + i);

      try { Thread.sleep(200); } catch (InterruptedException e) {}

    }

  }

 

  public static void main(String[] args) {

  

    NaiveDispatcher dispatcher = new NaiveDispatcher();

   

    dispatcher.queueRunnable(new CountingJob("Bob", 0, 5));

    dispatcher.queueRunnable(new CountingJob("Cat", 0, 5));

   

    dispatcher.run();

  }

}

 

The NaiveDispatcher class is basically the naïve dispatch queue implementation from the lecture slides with a main method to demonstrate it. CountingJob is a class which executes Runnable and can therefore be used as a job to schedule on the dispatch queue. CountingJob just contains an identifying name and start and length values for counting. It works by calling the static method printNumbers that prints out lines counting up from a start value to the specified length. It has a delay in it to simulate doing more work.

  1. Run the NaiveDispatcher program.

There is only a single thread in this program - the main thread. It first queues two counting jobs and then starts running the dispatch queue itself, executing one then the other of the two jobs. Once the jobs have been executed it will run forever in the dispatch loop, so you must forcibly terminate the program to end it.

  1. Once you have run the program, change the count values for both CountingJobs so they count up to 100. Run the program again and it should be fairly obvious that the first job blocks the dispatch queue for a long time. Forcibly terminate the program again.
  2. One way to fix this would be to run the CountingJob on background threads. To do this, change the run() method of CountingJob to the following, to call printNumbers on a new thread:

new Thread(() -> NaiveDispatcher.printNumbers(jobName, start, length)).start();

Run the program again. Now you will see the two CountingJobs interleaving as they run on separate threads. Remember to forcibly terminate the program as the main thread is still looping in the dispatch queue code.

But using a dispatch queue we can simulate concurrency with a single thread.

  1. Create the following class:

 public class Decomposer implements Runnable {

 

  private final NaiveDispatcher dispatcher;

  private final String jobName;

  private final int start;

  private final int length;

 

  private int current;

  private int segment = 5;

 

  public Decomposer(NaiveDispatcher dispatcher,

                    String jobName, int start, int length) {

    this.dispatcher = dispatcher;

    this.jobName = jobName;

    this.start = start;

    this.length = length;

    this.current = start;

  }

 

  @Override

  public void run() {

    int todo = Math.min(this.segment, this.length - this.current);

    int currentStart = this.current;

    this.dispatcher.queueRunnable(()->NaiveDispatcher.printNumbers(this.jobName, currentStart, todo));

    this.current += todo;

    if (this.current < this.length) {

      this.dispatcher.queueRunnable(this);

    }

  }

 

}

Decomposer is a version of CountingJob that breaks itself into a series of jobs on the dispatch queue.

Like CountingJob it implements Runnable and can be scheduled to run on the dispatch queue, but it has a reference to the dispatch queue and its run method does the following:

  • It queues a job to call printNumbers for a range of length 5 starting from where it is up to so far.
  • It keeps count in itself where the count is up to.
  • If it has not reached the end of its counting, it queues itself on the dispatch queue again and then returns.

Each execution of run() returns quickly having queued a short job to count 5, and another job to call run() again.

  1. Change the main method of NaiveDispatcher, commenting out the existing lines that queue CountingJobs, with equivalents that queue Decomposers:

//    dispatcher.queueRunnable(new CountingJob("Bob", 0, 100));

//    dispatcher.queueRunnable(new CountingJob("Cat", 0, 100));

   

    dispatcher.queueRunnable(new Decomposer(dispatcher, "Bob", 0, 100));

    dispatcher.queueRunnable(new Decomposer(dispatcher, "Cat", 0, 100));

Now run the program and see how we have simulated concurrency using only one thread and a dispatch queue.

 

 

 

Part 1 : Using Spring Boot Initializr to create a Maven project for a web application demonstration

Important Note

 

This document assumes you are using eclipse as your Java IDE. Unfortunately the version of Eclipse available via AppsAnywhere is not suitable. I recommend using IntelliJ IDEA from AppsAnywhere for this exercise as I know it works and will use it for live demonstrations. Although NetBeans should also work, I have experienced problems with it (mainly it seemed very slow to process Maven projects).

 

You can of course install Eclipse to any machine as follows (but I still recommend Intellij for working in the labs):

 

Visit: https://www.eclipse.org/downloads

Download and run the current eclipse installer on the lab machine and it will install in C:\Users\yourusename

  1. Create a Spring Boot project with the web dependency:

Visit https://start.spring.io and configure as follows:

 

  • Select Maven Project
  • Select Java Language
  • Select the latest non-snapshot or candidate version of spring
  • Set Artifact to "adp1"
  • Set Description to "ADP example 1 project for Spring Boot"
  • Set the Java version to whatever you have in eclipse (e.g. 17)
  • Add the Spring Web dependency.

Generate the project and download a zip archive that contains it. Select "Show in folder" from your download indicator and find the downloaded zip file in the explorer window. Right-click it and extract it (for this project it is OK to just do this in the downloads folder where you are, but if you prefer, move the extracted project folder somewhere else).

Examine the contents of the project folder - you will see there is not much there.

Burrow down to adp1\src\main\java\com\example\adp1 to see the automatically provided application source file.

Take a look at the Maven configuration file pom.xml (at the top level) and see if you can identify the Spring Web dependency that we added.

  1. Import into Eclipse and run the basic application

Now run eclipse.

On the File menu, choose the Import item and then find the Existing Maven Projects entry. Press the Next button and then set the root folder to the folder containing the pom.xml file for your downloaded project. Press Finish and wait while it imports, updates and builds the project.

Once it has done so, check the downloaded folder to see a few new files and directories have been added. Note that eclipse does not copy the project into its own working directory, it uses it wherever it has been saved. Now look at the project in the navigator in eclipse and inspect the Maven Dependencies node. Here you will find all the jar files that Maven has automatically downloaded and added to your project (these are actually stored in a directory Maven manages so they do not need to be duplicated for multiple projects).

In src/main/java you will be able to find the demo application file. Run this just as you would any other Java program. You will see Spring Boot's output on the console and log lines indicating that an embedded webserver (Tomcat) has been started. This demonstrates everything is working as it should, but the application does nothing so there is nothing to see. Terminate the application by stopping it with the IDE's terminate button.

  1. Demonstrate that there is a web server running

In eclipse, navigate to the folder src/main/resources/static

This is basically the web server's web root folder.

Create a file in here called index.html (right-click the static folder, choose new -> file and give the name index.html). Now edit this file by right-clicking it and saying open with text editor). Put the following HTML code in this file:

<html>

  <head>

    <title>Index Page</title>

  </head>

  <body>

    <h1>See, told you there was a webserver running!</h1>

  </body>

</html>

Now run the Spring Boot application again, and then open a web browser window and type localhost:8080/index.html into the address bar to see that this page gets served by the spring application. Terminate the Spring Boot application and then refresh the browser window to convince yourself it came from your program (it will no longer be there).

  1. Add a Rest Controller to your application to deliver dynamically generated content

In src/main/java/com.example.adp1 create a new class called HelloController.

Edit it so it contains the following, adding any imports as indicated by the IDE:

@RestController

class HelloController {

 

  @GetMapping("/")

  public String hello() {

    return "hello world!";

  }

 

}

Run the Spring Boot application again. Note that if you forgot to stop it last time it will fail to start saying that port 8080 is already in use. Always make sure to stop it before running with new changes.

Now visit localhost:8080/ in your web browser. You should see it says "hello world!".

What is happening?

OK, the main application class is annotated with @SpringBootApplication, and that makes LOTS of things happen behind the scenes when it is run. One thing that happens is that it automatically searches for classes in the application with recognized annotations on them. It finds our class HelloController and sees that it is annotated with @RestController. It knows that this means this class will contain methods to be invoked in response to HTTP requests received by the web server. It automatically connects up our class with the web server to make this happen. To do this, it looks at the methods declared in this class and finds our hello() method is annotated with @GetMapping.

Methods in @RestController classes that are annotated with @GetMapping will be invoked in response to HTTP GET requests. In brackets after the @GetMapping we specify the specific URI we want the method to respond to. In this case it is "/" which means the root of the website.

Besides @GetMapping, there are similar annotations for other kinds of HTTP requests, for example @PostMapping, @PutMapping and @DeleteMapping.

Because the @GetMapping annotation specifies the URI to respond to, we can have as many as we like in our controller class. Add the following method to the class after the hello() method:

@GetMapping("/helloagain")

public String[] helloAgain() {

  return new String[] { "hello world!", "again", "hang on what's this?" };

}

STOP the program and then run it again, and visit localhost:8080/helloagain in your browser.

Note that this method does not return a String, but a String array. What is displayed in your browser is actually a JSON representation of a String array:

["hello world!","again","hang on what's this?"]

The methods in a @RestController class convert their return type into a JSON representation automatically when sending to the web browser, and as we will soon see, automatically turn incoming JSON data into corresponding Java objects when reading the body of a request message.

Let's demonstrate this a little better. Create this new class in your project:

public class MyPOJO {

  private String firstName;

  private String lastName;

  private long idNumber;

  public MyPOJO(final String firstName,

                final String lastName,

                final long idNumber) {

    this.firstName = firstName;

    this.lastName = lastName;

    this.idNumber = idNumber;

  }

  public String getFirstName() {

    return this.firstName;

  }

  public String getLastName() {

    return this.lastName;

  }

  public long getIdNumber() {

    return this.idNumber;

  }

}

This class is called MyPOJO for a reason - POJO is an acronym of Plain Old Java Object - there is nothing special about it and it does not have to extend any ancestor classes or implement any specific interfaces. It is also what is called a Java Bean. Java Beans are just classes that have a get method for every field they contain, in which the method has the same name as the field except with get in front of it. This is a recognized convention that helps code automation. The JSON serialiser can only serialise objects that are Java Beans like this.

Now add the following method to our HelloController class.

@GetMapping("/pojo")

public MyPOJO[] myPojo() {

  return new MyPOJO[] {

    new MyPOJO("John", "Smith", 1234567),

    new MyPOJO("Sarah", "Brown", 7654321) };

}

STOP the program and then run it again, and visit localhost:8080/pojo in your browser.

You will see a JSON array of MyPOJO objects in the browser.

Now try the following method in the HelloController class:

@GetMapping("/pojo/{first}/{last}/{id}")

public MyPOJO myPojo( @PathVariable("first") String firstName,

                      @PathVariable("last") String lastName,

                      @PathVariable("id") int idNumber) {

  return new MyPOJO(firstName, lastName, idNumber);

}

This one will be invoked by a GET request that starts "pojo" and then has another three fragments following it. Note how the @GetMapping annotation uses names enclosed in curly brackets {} for the three following fragments, and note how the parameters of the method have been annotated with @PathVariable using these names to identify which fragment to use. The fragments of the URI are converted into the type of these variables and provided to the method automatically.

With this method in place, stop and restart the application and then observe the results of calling the following URIs in your web browser:

  • localhost:8080/pojo/Mickey/Mouse/77 (returns a newly created POJO object using the values in the URI, encoded into JSON)
  • localhost:8080/pojo/Mickey/Mouse (returns an error as we have no mapping for pojo followed by two fragments)
  • localhost:8080/pojo/Mickey/Mouse/Ooops (returns an error as we have no mapping for pojo followed by three STRING fragments, only a mapping for String, String, Int).

This is to illustrate the way the URL can be used in requests.

 

 

Part 2: Create a simple front-end interface

  1. Create HTML document

Web application front-ends are generally written in JavaScript (either directly, or indirectly using a language that compiles into JavaScript, such as Dart or TypeScript). We will create a crude but flexible interface for demonstration purposes.

The JavaScript program will be embedded in an HTML document that will be stored in the Spring Boot application's web root folder: src/main/java/resources/static.

You can create and edit this file in the IDE for your Spring Boot project, so start by creating a new file in that folder called generic-interface.html. If the IDE gives you a basic HTML template you can use that, if the file is empty paste the following into it:

<html>
<head>
<title>
ADP JSON tool</title>

 

</head>

  

<body>

 
</body>

</html>

You can see that the document as a whole is enclosed in an <html>…</html> tag, and inside it has two sections <head>…</head> and <body>…</body>. In general, the head section contains information about the document, the body section contains the content of the document. The <title> tag specifies what should appear on the web browser's tab.

Note

Everybody who intends to be involved in anything relating to computing should have a basic knowledge of HTML, if only to avoid embarrassment. If you know nothing about it, go to https://www.w3schools.com/html and follow the easy tutorials there. While you are there, you could also look at all their tutorials on other subjects too!

Although it is possible for JavaScript to create a DOM from scratch, for our simple interface we will declare the required components in static HTML (note that this does not mean the JavaScript couldn't replace or modify them later).

Add the following code to inside the <body>…</body> section:

<h3>HTTP method:</h3>

<select id='methodselector'>

    <option>GET</option>

    <option>POST</option>

    <option>PUT</option>

    <option>DELETE</option>

</select>

  

<h3>URI</h3>

<input type='url' id='urifield'>

  

<h3>JSON request body</h3>

<textarea id='jsonpayload'>

  

</textarea>

             

<button id='sendbutton'>Send</button>

  

<h3>Response</h3>

<div id='jsonreceived'>

    Response displayed here

  </div>

The <h3> tags identify the enclosed text as heading text to be displayed bigger.

The <select> tag and its nested <option> tags define a drop down choice component (like a JComboBox in Java Swing).

The <input> tag defines an input component, and in this case its type is set to url - this is basically a text field to type a url into.

The <textarea>…</textarea> tag defines a multi-line text entry field, where the user will be able to enter JSON data by hand.

The <button> tag defines a button.

The <div>…</div> tag defines a general placeholder - we will be adding text to this to display the response from the web server.

At this stage you should view the page in a web browser - although you could just view the file itself, it is better to view it served through your Spring Boot application as that will be necessary as it starts to actually do things. So, run the Spring Boot application and visit
localhost:8080/generic-interface.html in a web browser.

While viewing it in the web browser hit F12 to display the developer tools and find the DOM tree (in Chrome this is in the "Elements" tab). Note that this is NOT the source code of the file, it is the dynamic in-memory model the web browser uses. You might even see some elements or attributes that do not exist in the source code but have been added by the web browser as it created the DOM.

Note that many of the tags have been given an id attribute. This can be used to identify individual elements, either to apply style to them or to access them from JavaScript code.

  1. Add a little styling

Add the following tag containing CSS style rules in the <head> section of your source file:

<style>

  #jsonreceived {

    background-color: #ddddff;

    border: solid 1px black;

}

#jsonpayload, #urifield {

    width: 100%;

}

  </style>

These are style rules applied to tags identified by their id attribute. It makes the <div> for displaying response data have a background colour and a border. The second rule is applied to two tags identified by their ids - and just sets them to take up the full width of the window, just to look nicer.

  1. Add some JavaScript

JavaScript code is usually located in a separate file and linked to from a <script> tag in the <head> section, but it is also possible to include it directly in the file. Add the following to the <head> section of the file:

<script>

    function run() {

       document.getElementById("sendbutton").addEventListener("click", function() { send() });

    }

    

    function send() {
       alert("You called the send function!");
    }
</script>

This has declared two functions, but no immediately executable instructions. The run() function does the following:

It calls getElementById on the document object to obtain a reference to the DOM element with the id value "sendbutton". The document object represents the whole DOM tree and offers various functions to operate on it. The DOM element that is retrieved here is that created by the <button> tag. It is an object of type HTMLButtonElement and it has fields and methods just like an object in Java would. What happens next, is that the button object's addEventListener method is called with two parameters. The first is a string identifying the event to be listened to - a click on the element in this case. The second parameter is an anonymous function definition that just does one thing; it calls the send() function. This is just like adding an action listener object to a button in Swing, and passing a function to call is like passing a lambda expression.

At the moment, the send method will just put up a message dialog but do nothing else.

However, nothing is currently invoking either of these functions, so we need to add a directly executable script that will call run(). This must not be invoked before the <button> tag has been parsed and turned into a DOM object - because then the script would fail. The easiest way to do this is to add a fragment of script at the end of the document, so it only executes after the page has been rendered[1].

Add this fragment as the last thing inside the <body>…</body> section.

<script>

    run();

  </script>

Now when the browser loads the document and has created the DOM, the run() method will be called and register an event listener on the button. Then, when the user presses the button, the send() method will be called and display an alert.

Restart your Spring Boot server to make sure the button does this.

The send function is actually intended to send a network request based on the values in the interface components. The HTTP method will be set by the selector, the target URI by the URI field and any JSON data by the JSON request body field.

Replace the existing send function with this one:

function send() {

    const method = document.getElementById("methodselector").value;

    let uri = document.getElementById("urifield").value;

    // hack for empty field 

    if (uri == "") {

       uri = location.protocol + "//" + location.hostname + ":" + location.port;

    }

    let bodyText = document.getElementById("jsonpayload").value.trim();

    if (bodyText == "") {

       bodyText = "{}";

    }

    try {

       const bodyData = JSON.parse(bodyText);

       document.getElementById("jsonpayload").value = "";

       sendRequest(method, uri, bodyData, function(response, content) { responseHandler(response, content); });

    }catch(error) {

       alert(error);

    }

}

This function does not actually send the request, it simply extracts the various required parameters from the interface components. In each case the relevant DOM element is obtained by document.getElementById and its value obtained. There are a few tweaks - it the URI field is empty it constructs a URI pointing at the host name alone. If the body field is empty it replaces it with an empty JSON object {}. In the try-catch block the contents of the JSON text field is parsed as JSON, to show up any errors before sending (that is what the alert in the catch block is for). The JSON input field is cleared of its text in preparation for another request, and then the sendRequest function is called.

The sendRequest function is missing at the moment (it is down below) but what it expects as parameters are

  • method - a string containing the HTTP method to use - i.e. GET, POST, PUT or DELETE
  • uri - the address to send the request to
  • bodyData - a JavaScript object that will be converted to JSON and sent as the body of the message where appropriate (not for GET)
  • a function to invoke when the response is received, which takes the full HTTP response object, and its JSON content as parameters - in the code above this function merely calls the function responseHandler and passes both parameters on to it.

So, for this code to work we need both the sendRequest function itself, and the responseHandler function. Here is sendRequest:

/**

 * uri is destination

 * data is an object that will be sent as JSON data

 * handler is a function that accepts two parameters (see handleResponse below)

 */

async function sendRequest(method, uri, data = {}, handler = handleResponse) {

    const settings = {

       method: method,

       headers: { 'content-type': 'application/json' }

    };

    if (method != "GET") {

       settings.body = JSON.stringify(data);

    }

    try {

       const response = await fetch(uri, settings);

       const content = await response.json();

       if (response.ok) {

          handler(response, content);

       } else {

          alert('Error!  Status = ' + response.status);

       }

    } catch (error) {

       console.log(error.stack);

       alert('Error: ' + error);

    }

}

This is very similar to the function that was included in the lecture slides. The variable settings is created as an object with a field method, containing the value provided for the HTTP method, and a field headers, which contains an object that contains a single field content-type with the value application/json. Note how the object assigned to settings is basically JSON syntax used inline in JavaScript[2]. Then, if the HTTP method provided is not GET, the settings object gets an additional field added called body, which is set to the JSON string for the data object that was provided to the method.

The settings object is used to create the HTTP message that will be sent across the network. The content-type header is a commonly used HTTP header that specifies how the data in the body should be interpreted - and the value here is declaring that it is JSON data.

We then have a try-catch block that asynchronously calls the fetch method to send the request and receive the response and then asynchronously retrieve the JSON as an object. If a system error occurs the catch block will be invoked (and display an alert box) otherwise, if the response has an HTTP code that indicates success, the function provided as the handler is invoked. If the response has an HTTP error code (such as 404 Not found if the URI does not exist or 500 Server error if your Spring Boot application threw an exception while handling the request) an alert is displayed with the error code and the handler is not invoked.

The handler function appears below:

function responseHandler(response, content) {
    document.getElementById('jsonreceived').innerHTML = '<pre>' + JSON.stringify(content, null, 4) + '</pre>';
}

This function is very simple; all it does is put the body of the response message in the JSON received window. It does assume the body is valid JSON however - and in the case of our default "hello world" return value, that does not hold true. You will see an error because of this if you visit localhost:8080 alone (or an empty string in the URI field). On the other hand helloagain and pojo both work because they return JSON data.

  1. Demonstrate HTTP methods

Leave the HTTP method on GET and type helloagain in the URI, then press send. This invokes localhost:8080/helloagain.

Try pojo, then try pojo/jimi/hendrix/123.

In each case you should see the expected JSON data displayed in the response area.

Now try something that doesn't exist (e.g. poo) and you should see a 404 error message in an alert.

Try an empty field (equivalent to localhost:8080) and you should see the error generated by trying to parse "hello world" as JSON when it is not.

Try typing index.html in the field and see what happens. Do you get what happened there?

Now try POST and the URI pojo. You will get a 405 ("method not allowed") code. This is because while there is a mapping to the URI it is only for GET requests. Let's add an endpoint to HelloController in the Spring Boot application for POST requests sent to pojo.

@PostMapping("/pojo")
public MyPOJO postPojo(@RequestBody MyPOJO body) {
   
return body;
}

This method uses @RequestBody to define one of its input parameters. This means that Spring will read the request body and try and turn it into the specified object. In this case it means it will try and convert some JSON received from the client into an instance of MyPOJO. Then it simply returns the same object (which will be emitted by converting back into JSON).

Start and stop your Spring Boot application and try POST and pojo, with an empty request body field. Quite surprisingly, we get back a MyPOJO object with null first and last names and a 0 for the idNumber! Spring converted your empty message body into a MyPOJO object with no values in it.

Now paste this JSON into the request body field and try again:

{

    "firstName": "frank",

    "idNumber": 22,

    "lastName": "hill"

}

The call should complete normally and the corresponding values come back. Note that it did not matter what order the fields are given in.

Now try

{

    "idNumber": 226,

    "fruit": "Banana"

}

You will get a MyPOJO object with only the idNumber field filled in. The fruit value was ignored because MyPOJO does not have one. In fact, any JSON will be accepted, and will return a MyPOJO object, and only the fields that were recognized will be set.

Note: That Spring will create MyPOJO objects at all is rather surprising - most JSON deserialisation libraries require that the class to deserialise into has a no-arguments constructor and setter methods for each field to be written. The deserialiser used by Spring Boot (it's called Jackson) appears to recognize a parametered constructor and make use of it (or else uses reflection to write the private fields).

 

  1. Fake some persistence and demonstrate REST functionality

In the REST architecture the methods GET, POST, PUT and DELETE have specific meanings, related to a persistent store of items. GET fetches an item based on some kind of id, POST adds a new item (usually allowing the system to generate an id code for it), PUT replaces an item with a specified id and DELETE deletes an item with a specified id.

Persistence generally means storing the items in a database, but for now we will just store them in the runtime memory of the server as ordinary program variables. This means that they will only persist as long as the server is running, but that will be OK for now.

Add this field to the HelloController class:

private final TreeMap<Long,MyPOJO> map = new TreeMap<>();

This map will store MyPOJO objects, using their id number as the key. You will need to import TreeMap.

First, let's improve our basic GET pojo end point. At the moment it returns an array containing two fixed values. Comment out that method and add this replacement instead:

@GetMapping("/pojo")
public Collection<MyPOJO> getAll() {
   
return map.values();
}

You will need to import Collection. This just returns all the MyPOJO objects in the map (if there are none you will get an empty array back).

OK, now we want a classic REST get-by-id endpoint. Add this code, which maps to the URI pojo/{id} and returns the specified MyPOJO from the map:

@GetMapping("/pojo/{id}")
public MyPOJO getById(@PathVariable("id") Long id) {
   
return map.get(id);
}

Of course, we have no way of putting objects in the map yet. Let's implement REST POST functionality. For this we need something a bit fancier. REPLACE the existing POST mapping with this one:

@PostMapping("/pojo")
public MyPOJO postPojo(@RequestBody MyPOJO body) {
   
long nextId = 1;
   
if (!this.map.isEmpty()) {
        nextId = ((SortedSet<Long>)
this.map.keySet()).last() + 1;
    }
    MyPOJO newItem =
new MyPOJO(body.getFirstName(), body.getLastName(), nextId);
   
this.map.put(nextId, newItem);
   
return newItem;
}

This takes the first and last names specified in the JSON body of the request, and creates a new ID number being the biggest in use plus one. Then it constructs a new MyPOJO object, stores it in the map and returns it to the caller.

Now experiment by listing all the MyPOJO objects with GET pojo, then use POST pojo to create some objects (one at a time):

{ "firstName":"mickey", "lastName":"mouse" }

{ "firstName":"minnie", "lastName":"mouse" }

{ "firstName":"donald", "lastName":"duck" }

List them all again.

Now try GET pojo/2 to make sure you can access them individually by id number.

Now let's complete the REST functionality with PUT and DELETE.

PUT is similar to POST but simpler. We use the ID number in the incoming MyPOJO object to update it in the map.

@PutMapping("/pojo")
public MyPOJO putPojo(@RequestBody MyPOJO body) {
   
this.map.put(body.getIdNumber(), body);
   
return body;
}

And DELETE is very simple. Here I am returning a String array with a message in as a simple way of getting valid JSON sent back.

@DeleteMapping("/pojo/{id}")
public String[] deletePojo(@PathVariable long id) {
   
if (this.map.remove(id) != null) {
       
return new String[] {"item removed"};
    }
else {
       
return new String[] {"item not found"};
    }
}

Make sure these endpoints work as you expect. If you create

{ "firstName":"minnie", "lastName":"mouse" }

with ID number 2 again, you could use PUT to update it as follows:

{ "firstName":"Minnie", "lastName":"Mouse", "idNumber": 2 }

 

  1. Things to try

 

  • Create a POST endpoint that accepts an array of MyPOJO objects and adds them all to the map in one go.
  • Create a PUT endpoint that accepts an array of MyPOJO objects and updates them all in the map in one go.
  • Create a DELETE endpoint without any id number that clears the map.

 


[1] There are more elegant ways to do this, but this is OK for our purposes.

[2] Note that field names are quoted in formal JSON, but need not be when used in JavaScript directly.

 

Expert answer

 

This Question Hasn’t Been Answered Yet! Do You Want an Accurate, Detailed, and Original Model Answer for This Question?

 

Ask an expert

 

Stuck Looking For A Model Original Answer To This Or Any Other
Question?


Related Questions

What Clients Say About Us

WhatsApp us