Skip to content

Task 1

Your first task is the manual conversion of the Book Store into a RESTful service.

WARNING! Start your screen recording now. Your entire on-screen task activity must be captured. This includes your familiarization with the task description!

Task Illustration (Restifying the Zoo)

I will now illustrate the steps of a manual conversion to a RESTful service on the example of the Zoo.
I will use the previously shown Zoo REST interface description.

Afterwards, your task will be to apply the same manual methodology on a different application. Your task is not to replicate the Zoo conversion while you are watching.

  • Please watch this screencast where I demonstrate and explain the required code changes.
    • Below you find a summary of the main steps.
    • If anything does not work as expected, take a look at the Troubleshoot section.
  • Additionally you can conveniently inspect both versions and the changes made:

Project Layout Overview

A manual RESTification is a series of five activities:

Loading Legacy Sources into IDE

  • Start IntelliJ.
  • Use the "Open" option to get to the selection dialogue: open-1
  • Select the cloned project root folder, then click the "Open" button:
    open-2

Build Configuration Changes

Want to see all Zoo-RESTify pom.xml changes at a glance? Run git diff master..RESTified pom.xml. Green lines were added for RESTification, red lines were removed.

  • Artifact properties:
    • Adjust artifactId. Change suffix to "restified"
    • Adjust name. Change suffix to "restified"
  • Update developer information:
    • Remove the email and organizationUrl lines
    • Change name to your personal codename, e.g. "blue snail" (see my email)
    • Change organization to "mcgill.ca"
  • Parent:
    • Declare inheritance from spring boot parent.
      Place below snippet right after the <license>...</license> block:
      <!-- this parent block lets this maven project extend a prepared spring specific template.-->
      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
      </parent>
      
  • Dependencies:
    • Declare dependency towards spring boot artifact:
      <!-- This block adds spring boot as a dependency, so we can use the parent pom configuration and non-standard annotations.-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.3.0.RELEASE</version>
      </dependency>
      
  • Final Name
    • Add "Restified" as suffix to the finalName attribute value
  • Build plugins:
    • Remove plugin for legacy launcher class:
      <!-- specify main class for exec goal -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.6.0</version>
        <executions>
          <execution>
            <goals>
              <goal>java</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <mainClass>eu.kartoffelquadrat.zoo.DesktopLauncher</mainClass>
        </configuration>
      </plugin>
      
    • Remove plugin for legacy compilation to self contained JAR:
      <!-- specify main class for JAR manifest-->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
          <archive>
            <manifest>
              <mainClass>eu.kartoffelquadrat.zoo.DesktopLauncher</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      
    • Add plugin for spring-boot launcher class: (Don't create the new launcher class yet!)
      Don't forget to adapt the mainClass tag!
      <!-- Spring specific build plugin, produces self contained JAR with default launcher class.-->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <fork>true</fork>
          <!-- Replace "zoo" by actual package name in next line! -->
          <mainClass>eu.kartoffelquadrat.zoo.RestLauncher</mainClass>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      

Note: Some changes might note take full effect until you manually reload the pom.xml file. To do so, right click the pom.xml file and select: Maven -> Reload Project

Java Code Changes

Hint: You can use git to see all java changes made to the Zoo for manual RESTification.
Run: git diff master..RESTified *java
Green lines were added for RESTification, red lines were removed.

Remove conflicting files

You have to delete two things:

  • The legacy launcher, located in src/main/java/eu/karotffelquadrat/*/DesktopLauncher.java
  • The legacy unit tests. Remove the entire test folder: src/test
    (This part has been skipped in the Zoo explanatory video, since there were no test classes)

Launcher

First thing to do is the creation of a new launcher class. It should be placed anywhere within the src/main/java/eu/kartoffelquadrat/... directory.

  • Create it by right clicking on the eu.kartoffelquadrat... package:
    create

  • Enter RestLauncher as class name, do not type the .java extension. IntelliJ will handle that for you.
    entername

Code of the RestLauncher.java class. (Replaces the legacy launcher)

package eu.kartoffelquadrat.zoo;  <----- Replace "zoo" by whatever application you are working on

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * This class powers up Spring and ensures the annotated controllers are detected.
 */
@SpringBootApplication
public class RestLauncher {
    public static void main(String[] args) {

        SpringApplication.run(RestLauncher.class, args);
    }
}

Beans and Singletons

  • Spring creates new instances of classes annotated with:
    @RestController
    
  • The existing singleton pattern is bypassed, since Spring uses reflection to gain constructor access, even if the declared constructor is private.
  • Having both @RestController and a getInstance method in the same class is dangerous.
    There are two ways to side-step inconsistency issues:
  • If you chose Autowiring, you do not create new classes. You directly annotate the relevant existing classes with @RestController.
  • This tells Spring to create one instance per annotated class, using a default constructor.
  • Any existing singleton pattern is therefore obsolete: If you annotated a class with @RestController, make sure to remove the singleton pattern (the getInstance method) and make the default constructor public.
  • Whenever an instance of such an annotated class is required, you can obtain the spring maintained instance with @Autowired.
    Example:
    package eu.kartoffelquadrat.zoo;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class FooController {
    
        /*
        @Autowired ensures the zoo field is set after instantiation of FooController, given Zoo is annotated with @RestController.
        */
        @Autowired
        Zoo zoo;
    
        public void bar() {
        // Here you can access the zoo instance using the local, autowired field (instead of calling the obsolete getInstance method)
        zoo.getOpeningHours();
        }
    }
    

    Autowired fields are only accessible after class instantiation. Do not invoke methods that require autowired values from a constructor, or you will get a NullPointerException at runtime. Instead annotate those methods with @PostConstruct. This advises spring to invoke a method after class instantiation.

  • If you chose Proxy Classes, you do annotate the existing classes. You instead replicate every existing relevant class and place all annotations in the replica. The replica acts as proxy that forwards every method call to the original, using getInstance().
  • Keep the singleton pattern in the original java classes. They remain untouched. Instead place an @RestController annotation in every proxy class created.
    Example:
    package eu.kartoffelquadrat.zoo;
    
    import org.springframework.web.bind.annotation.RestController;
    
    /**
    This Proxy class is decorated with @RestController, instead of the original singleton class. Any required method of the original class can be proxied with an internal getInstance call.
    */
    @RestController
    public class ZooController {
    
        /** Proxied access to a method of the original / singleton class.
    // Mapping annotation goes here.
        public OpeningHours getOpeningHours() {
            // Access to the original class is achieved with a call to getInstance.
            return Zoo.getInstance().getOpeningHours();
        }
    }
    

Resource Mapping with Annotations

Note: Below annotation syntax can only be used in classes annotated with @RestController.

  • Resource mappings (these annotations decorate functions)

    • Annotations types:
    Get Put Post Delete
    @GetMapping("...") @PutMapping("...") @PostMapping("...") @DeleteMapping("...")
    • Arguments:
      • Static: Full resource path within quotes, e.g.: "zoo/animals"
      • Dynamic: Full resource path within quotes. Any dynamic placeholder on path is marked by curly brackets, e.g.:"zoo/animals/{animalname}"
  • Parameter mapping (these annotations decorate function parameters)

    • Pathvariable:
      @PathVariable("animalname")
    • Request body:
      @RequestBody

Build and Run

These instructions are to build and run from command line. While developing in IntelliJ you can safly use the "green triangle button" next to your new spring launcher class.
greenbutton

The first time you start your REST application you might see a warning about incoming connections. Select "Allow". firewall

At some point you also need to build your application into a self contained artifact:

  • Build a self contained executable jar file: mvn clean package
  • Run the jar file: java -jar target/zoorestified.jar

    zoorestified is the name you provided as finalName in your pom.xml.

  • Build a self contained executable jar file: mvn clean package
  • Run the jar file: java -jar target\zoorestified.jar

    zoorestified is the name you provided as finalName in your pom.xml.

Compile and Test

To compile and test your produced REST service:

  • Compile your project with: mvn clean package
  • Open the newly generated target folder.
  • Run the generated JAR file with: java -jar YOUR-SERVICE-NAME.jar
  • Test access with your browser. You can directly access any [GET] resource by typing this into the URL bar:
    http://127.0.0.1:8080/zoo/animals
    (where zoo/animals is replaced by a valid GET-enabled resource of your application)

Your Task

Now it is your turn!

WARNING! Double check that your screen recording is still running. Your entire on-screen task activity must be captured.

Instructions

  • All you need to do is replicate the above steps for the requested app.
  • Limits:
    • You may stop whenever you deem the task successfully completed.
    • You may also stop after 90 Minutes of refactoring, whatever the state of your refactoring - You are however also allowed to continue as long as you want.
  • The target REST interface description for your task is below.
  • Please now run a manual conversion of the Book Store into a RESTful service.

Legacy Application Details

Below diagram highlights classes and methods of the legacy Book Store application, relevant to your RESTification task. For more information on theses methods, also consult the Book Store online documentation.
bookstore-classes

Interface Description: Book Store Resources and Methods

  • Your interface should begin with a top-level resource "bookstore", not offering any methods.
  • "bookstore" has two subresources, "isbns" and "stocklocations", both offering a [GET] method.
    • A [GET] request to "isbns" should result in a listing of all isbn numbers stored in the system.
    • A [GET] request to "stocklocations" should result in a listing of all geographic store locations.
  • The "isbns" resource should have a single dynamic placeholder subresource representing an isbn number,
    • A [GET] request to the dynamic placeholder subresource should provide details on a given book, identified by isbn number which serves as input parameter.
    • A [PUT] request to the dynamic placeholder subresource should allow adding a new book to the system. All details on the book are passed as request body payload. (Note: This might look a bit peculiar to not use the value of the dynamic placeholder "isbn" for a subsequent mapping. That is ok here, because the required ISBN information is also contained in the body payload object. We do not want you to add any additional validation here, to keep things simple.)
  • The dynamic placeholder resource should have a child resource "comments", representing comments for a given book, identified by isbn. The value of the parent placeholder resource determines which book is targeted.
    • A [GET] request to the "comments" resource should result in a listing of all comments for the specified book. The value of the parent resource representing an isbn number servers as input parameter. The result should index comments by their id.
    • A [POST] request to the "comments" resource should allow the creation of new comments. The id of the new comment is generated on server side and not required, however, again the parent placeholder resource encodes the isbn of the targeted book. The comment itself is to be transmitted as request body payload.
    • A [DELETE] request to the "comments" resource should delete all comments for a given book, identified by the isbn number of the parent dynamic placeholder resource.
  • The "comments resource should have a dynamic subresource representing a specific comment by id. It offers two methods: [POST] and [DELETE].
    • A [POST] request to specific comment should allow to alter the content of that comment. Target book and target comment are respectively identified by the dynamic resource itself and the corresponding grandparent placeholder resource. Similar to comment creation, the new comment content is tranmitted as request body payload.
    • A [DELETE] request to specific comment should allow removal of an existing comment. Target book and target comment are respectively identified by the dynamic resource itself and the corresponding grandparent placeholder resource.
  • The "stocklocations" resource shoud have a dynamic subresource representing a specific geographic location (city name).
    • A [GET] request to a specific location should return the exact amount of book copies in stock for the given location, as a map indexed by isbn number. The path variable itself providing the target location can serve as argument for a corresponding method call.
  • Finally, the dynamic resource representing a specific geographic location should itself have a dynamic subresource representing the stock for a given book at the given location.
    • A [GET] request on this dynamic resource should return the amount of copies in stock for a book specified by isbn (the value of this placeholder resources) and stock location (the value of this resource's parent placeholder resource)
    • A [POST] request on this dynamic resource should update the current amount of copies for a given book. Target location and isbn are likewise encoded by this placeholder resource and its parent placeholder resource. The new amount is provided as request body payload.

Click here to download interface description as file.

Troubleshoot

  • Q: I open the project with IntelliJ, but everything is underlined in red.
    A: The projet was not correctly opened. There are multiple potential fixes:
    Option 1) Reload pom.xml: Right click the file, then select Maven -> Reload Project.
    reload Option 2) Verify the JDK version: Select File -> Project Structure.... Verify 11.0.5 is selected in the Project and SDKs tab:
    sdk1
    sdk2
    Option 3) Invalidate IntelliJ caches: Select File -> Invalidate Caches.... Then select the first two checkboxes:
    invalidate1
    invalidate2 Option 4) Delete the cloned folder, clone the repository again, then make sure to open the project exactly as shown.
  • Q: I cannot compile / run the project, the green button is greyed out.
    A: The project has no launch configuration by default, therefore the arrow in the top bar is not available. Open the RestLauncher class instead and click on one of the green triangles, left of the code.
  • Q: I RESTified the application, but when I start it there is a Nullpointer-Exception.
    A: Most likely the constructor code in one of the classes annotated with @RestController invokes a call to an @Autowired field. Autowiring is only available after class initialization (after the constructor). Do not call any method with access to autowired fields in a constructor. Instead tell spring to call it after class initialization. Use the @PostConstruct annotation. See @PostConstruct.
  • Q: I've made a mistake on project import, how can I start from scratch?
    Delete the cloned folder, clone the repository again, then make sure to open the project exactly as shown.
  • Q: I've modified the pom.xml file as shown, but IntelliJ still does not seem to know about Spring.
    A: Sometimes the changes made to the pom.xml are not automatically detected. (See first question, pom.xmlreload```.)
  • Q: IntelliJ asks me whether I want to trust the project sources. Should I?
    A: Yes. This is just a security mechanism to prevent malicious code being executed on project import. The provided sources are all from us and can be trusted.
    trust