Your task is to create a RESTful web service endpoint that allows a client to input a product name as a GET query parameter and returns the cheapest product.
Provided below are API keys for the BestBuy and Walmart APIs. Your result should return the best (minimum) price for the product and which store to buy it from. If there are multiple products, always return the lowest priced product. For example:
Request
GET /product/search?name=ipad
Example Response
200 OK
{
"productName": "iPad Mini",
"bestPrice": "150.00",
"currency": "CAD",
"location": "Walmart"
}
A second optional task is another RESTful web service endpoint that allows a client to input an email address and product name as a JSON object in the HTTP body.
Using the EmailService
, every time the minimum price decreases for the specified item, send an email to the specified email address indicating that the price has dropped.
Example Request
POST /product/alert
{
"productName": "ipad",
"email": "[email protected]"
}
Example Response
200 OK
(no response body)
Example Email
Sending email to '[email protected]' with subject 'The price of the ipad has dropped!' and message 'The price of the ipad has dropped from 150.00CAD to 148.00CAD! Get it quick!'
Note that when using the EmailService
, no emails are actually sent. A string similar to the example email will be logged in the console.
BestBuy: pfe9fpy68yg28hvvma49sc89
Walmart: rm25tyum3p9jm9x9x7zxshfa
Commit this project to your own Git repository on Github or Bitbucket and email us the link to the repository.
NOTE 1: Commands and file paths in this documentation are relative to the project root unless otherwise specified.
This starter kit is an example of the systems we use at BlueSpurs. We want to avoid time spent setting up environments and worrying about boilerplate. The goals are:
- Reduce project start-up friction.
- Introduce a common base for all projects.
- Reduce developer environment dependencies.
- Standardize deployments.
- Follow best practices!
- Gradle build tools.
- Liquibase database migrations and changeset generation based on JPA entity changes.
- Hibernate as the JPA vendor.
- Input validation using the Hibernate Validator.
- Unit tests.
- In-memory caching by default, configurable to use other providers such as Redis, Memcache, or EhCache.
- Cross-origin resource sharing (CORS) enabled by default.
- Spring projects including Boot, Data, Security, Devtools, and HATEOAS.
- Tomcat hooks for WAR container deployment.
- Logging using SLF4J.
- Test code coverage reporting.
To get started, you'll need 2 things:
- The Java JDK (development kit) version 1.8.
- The text editor or IDE of your choice.
Additional setup instructions are provided for IntelliJ IDEA and Eclipse.
Configuration values can be found in the src/main/resources/application.properties
file. For a development environment, these defaults should work out of the box.
To run the project, issue the command ./gradlew bootRun
(on Windows, omit ./
). After some time, you should be able to open http://localhost:8080
in your web browser to see the text, "The Bluespurs Spring Starter Kit is running properly."
You can run tests using gradle by executing ./gradlew cleanTest test
.
After opening IntelliJ, if you have any projects open, choose File -> Close Project
.
From the start screen, follow these steps:
- Open project...
- Navigate to the
settings.gradle
file and select it. - In the gradle settings dialog, enable
Use auto-import
and set the JVM to version 1.8. - Click OK. Once IntelliJ is finished building the project, navigate to
Run -> Edit Configurations...
. - Create a new configuration (click the
+
icon) and chooseGradle
.- Name it anything, like
Development Server
. - Enable
Single instance only
. - Choose the gradle project.
- Set the task to
bootRun
.
- Name it anything, like
- Create a new configuration and choose
JUnit
.- Name it
Unit Tests
. - Set the test kind to
Category
. - Set the category to
com.bluespurs.starterkit.UnitTest
. - Set the search to
Whole project
.
- Name it
Now that the run configurations are set up, you can run or debug the application and it's tests. Once the development server is running, it is not necessary to reboot it after every change. Simply recompile the project (Build -> Make Project
or Ctrl-F9
) and the changes will be reloaded.
These instructions are for Eclipse Mars using the regular flavour of Eclipse (Eclipse Java SE or Eclipse for Java Developers - not Eclipse Java EE).
- In Eclipse, follow
File -> Import...
and expandGradle
to selectGradle Project
. - Choose
Next
. - Select the root directory. Leave all of the settings as-is and navigate through the dialogs. Click
Finish
. - Eclipse will build the project.
- Navigate to
Run -> Run Configurations
. - Add a new
Gradle Project
run configuration:- Name it anything, like
Development Server
. - Set the task to
bootRun
.
- Name it anything, like
- Add a new
JUnit
run configuration:- Name it anything, like
Tests
. - Select
Run all tests in the selected project
. - Set the test runner to
JUnit 4
.
- Name it anything, like
You can now run the application and it's tests.
The Spring Framework uses a variety of design patterns that users should familiarize themselves with in order to take advantage of the framework in full. Spring makes an effort to make itself more manageable by providing Spring Boot. Spring Boot helps developers get started writing production-grade Java applications with very little configuration. Most of the configuration is handled automatically with the ability to be overridden as needed by the developer.
A comprehensive overview of Spring Boot can be found here: Spring Boot v1.3.2.RELEASE single page documentation
As much as possible, following the S.O.L.I.D software design principles when developing software leads to systems that, over time, tend to be more extensible and maintainable while removing code smells. Spring embraces such principles.
Prior to Spring version 4, most configuration was done using XML files. Today, annotation driven configuration is encouraged. This starter kit uses annotation driven configuration exclusively.
A brief description of the technologies used in the starter kit:
- REST (Representation State Transfer) is an architectural style used to build applications that embrace HTTP as the data transport protocol. HATEOAS (Hypermedia as the Engine of Application State) is used for service discovery in REST applications.
- Gradle is a build tool which manages the compiling/packaging process and other tasks, such as downloading other Java packages (typically JAR files) that are depended on. Spring Boot is one such dependency that is managed by Gradle.
- Spring is a Java framework. It is comprised of many projects (such as Spring Boot or Spring Security) that can be used interchangeably. For example, it is possible to use the Spring Web project without using Spring Boot. Some projects complement other projects, but can be interchanged with other implementations due to the Java Specification Requests (JSR) which set standards for frameworks such as Spring.
- JPA is the Java Persistence API which, as the result of a JSR, provides a standard interface for interacting with different database systems. It is important to understand that the JPA is just a specification.
- Hibernate is an implementation of the JPA. Along with other capability, Hibernate offers a concrete implementation of the JPA. There are other implementations such as EclipseLink. In Java terms: if the JPA is the interface, then Hibernate is the class.
- Spring Data is an abstraction of the JPA. Because the JPA is complex, the Spring Framework offers a project called Spring Data which offers an easier-to-use API for working with the JPA. Repositories (explained later) are an example of this abstraction.
- Liquibase is a utility used for making changes to the database. In production, managing changes to the database is tricky. Liquibase makes such changes standardized and manageable.
If you're new to these technologies, it is important to understand their respective roles in the development process.
- Inversion of Control with Dependency Injection (IoC and DI): Rather than having objects that define and instantiate their own associations (dependencies), IoC is the idea that objects only need to define their dependencies, and have an external object (often called the container) instantiate the correct instances. DI is a design pattern that enables IoC.
- Data Transfer Objects (DTO): A simple method of representing data to be transferred from one system to another (common prior to serialization). This starter kit uses the DTO pattern in two places: input and output. Input classes are stored in
com.bluespurs.starterkit.controller.request
and classes are stored incom.bluespurs.starterkit.controller.resource
. These classes are only used to transfer data into and out of the system. - Repositories and Specifications: Repositories are a domain-specific (business-specific) abstraction to Data Access Objects (DAO), a common design pattern for CRUD (create, read, update, delete) operations on a database. Because we use the JPA to manage low-level data access, we avoid the need for DAO which allows us to think about software in a more domain-oriented sense. Specifications are simple objects used to locate data in a decoupled, reusable manor.
In many Java applications (not exclusive to Spring), it is common to see a 3-tier architecture that resembles an MVC structure.
There is no problem in computer science that cannot be solved by adding another layer of indirection, except having too many layers of indirection. - David Wheeler or Butler Lampson
- Controllers handle I/O and talk to services.
- Services perform business logic (including transactional work and caching) and talk to repositories.
- Repositories manage data access in a domain context and talk to the DAO. Because our DAO is provided by the JPA, our layering stops here.
You may have noticed that this is a layered architecture. For example, controllers should not talk to repositories directly. Layers should only communicate with those directly above or below it. This, of course, is a pattern not a rule.
For the most part, the database schema will be managed by Liquibase. Liquibase uses JPA entity annotations to construct a schema that will work with your domain objects. Because of this, making schema changes has a typical workflow:
- Make your changes to the classes inside
com.bluespurs.starterkit.domain
. Be sure to include the appropriate annotations and include mutation methods. - Use your modified domain objects as if the schema exists to build your feature.
- Once content with the changes, run
./gradlew diffChangeLog
. This writes schema changes to the Liquibase changelog located insrc/main/resources/db-changelog.xml
. - Make sure the changes discovered by Liquibase meet your expectations.
- Run Liquibase by re-compiling your project with the development server running, or by using gradle.
Input validation is handled by the Hibernate Validator library. Validation can be done on user input (namely POST and PUT HTTP verbs) by creating a class inside of com.bluespurs.starterkit.controller.request
and annotating the fields appropriately. For example, a registration input class may look like this:
public class UserRegistrationRequest {
@NotBlank
private String name;
@NotBlank
@Email
private String email;
@NotNull
@Length(min = 8, max = 72)
private String password;
// Getters and setters omitted.
}
To use this class, simply accept it as an argument to any controller method (make sure to annotate it with @Valid @RequestBody
to tell Spring that it needs to be validated and that the data comes from the HTTP request body):
@RestController
class MyController {
@RequestMapping(method = RequestMethod.POST)
public void registerNewUser(@Valid @RequestBody UserRegistrationRequest request) {
// Use getter and setter methods to access user input.
}
}
If validation fails, Spring will terminate the request and generate an HTTP 400 Bad Request response.
The starter kit is only configured to run inside of a Tomcat servlet container. To generate the WAR file, run: ./gradlew bootRepackage
. For this project, bootRepackage
has been configured to depend on test
, meaning that a WAR file cannot be deployed if any tests are failing.