- Purpose
- to demonstrate the basic functionality of the Spring Framework
- to demonstrate API testing via Postman
- Domain objects are the backbone for an application and contain the business logic.
- Create a sub package of
io.zipcoder.tc_spring_poll_application
nameddomain
.
-
Create an
Option
class in thedomain
sub-package. -
Option
class signature is annotated with@Entity
-
Option
has anid
instance variable of typeLong
id
should beannotated
with@Id
- denotes primary key of this entity
@GeneratedValue
- configures the way of increment of the specified
column(field)
- configures the way of increment of the specified
@Column(name = "OPTION_ID")
- specifies mapped column for a persistent property or field
- without
@Column
specified, the framework assumes the field's variable-name is the persistent property name.
-
Option
has avalue
instance variable of typeString
value
should beannotated
with@Column(name = "OPTION_VALUE")
-
Create a
getter
andsetter
for each of the respective instance variables.
-
Create a
Poll
class in thedomain
sub-package. -
Poll
class signature is annotated with@Entity
-
Poll
has anid
instance variable of typeLong
id
should beannotated
with@Id
@GeneratedValue
Column(name = "POLL_ID")
-
Poll
has aquestion
instance variable of typeString
question
should beannotated
with@Column(name = "QUESTION")
-
Poll
has anoptions
instance variable of typeSet
ofOption
options
should beannotated
with@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "POLL_ID")
@OrderBy
-
Create a
getter
andsetter
for each of the respective instance variables.
-
Create a
Vote
class in thedomain
sub-package. -
Vote
class signature is annotated with@Entity
-
Vote
has anid
instance variable of typeLong
id
should beannotated
with@Id
@GeneratedValue
Column(name = "VOTE_ID")
-
Vote
has aoption
instance variable of typeOption
option
should beannotated
with@ManyToOne
@JoinColumn(name = "OPTION_ID")
-
Create a
getter
andsetter
for each of the respective instance variables.
- Repositories or Data Access Objects (DAO), provide an abstraction for interacting with datastores.
- Typically DAOs include an interface that provides a set of finder methods such as
findById
,findAll
, for retrieving data, and methods to persist and delete data. - It is customary to have one
Repository
perdomain
object. - Create a sub-package of
io.zipcoder.tc_spring_poll_application
namedrepositories
.
- Create an
OptionRepository
interface in therepositories
subpackage. OptionRepository
is a subclass ofCrudRepository<Option, Long>
- Create a
PollRepository
interface in therepositories
subpackage. PollRepository
is a subclass ofCrudRepository<Poll, Long>
- Create a
VoteRepository
interface in therepositories
subpackage. VoteRepository
is a subclass ofCrudRepository<Vote, Long>
- Controllers provides all of the necessary endpoints to access and manipulate respective domain objects.
- REST resources are identified using URI endpoints.
- Create a sub package of
io.zipcoder.tc_spring_poll_application
namedcontroller
.
-
Create a
PollController
class in thecontroller
sub package.PollController
signature should beannotated
with@RestController
-
PollController
has apollRepository
instance variable of typePollRepository
-
Create a constructor that accepts a
PollRepository
argument and assigns its value to thepollRepository
member variable.- Mark the constructor with the
@Autowired
annotation.
- Mark the constructor with the
- The method definition below supplies a
GET
request on the/polls
endpoint which provides a collection of all of the polls available in the QuickPolls application. Copy and paste this into yourPollController
class.
@RequestMapping(value="/polls", method= RequestMethod.GET)
public ResponseEntity<Iterable<Poll>> getAllPolls() {
Iterable<Poll> allPolls = pollRepository.findAll();
return new ResponseEntity<>(allPolls, HttpStatus.OK);
}
- The method above begins with reading all of the polls using the
PollRepository
. - We then create an instance of
ResponseEntity
and pass inPoll
data and theHttpStatus.OK
status value. - The
Poll
data becomes part of the response body andOK
(code 200) becomes the response status code.
- Ensure that the
start-class
tag in yourpom.xml
encapsulatesio.zipcoder.springdemo.QuickPollApplication
- Open a command line and navigate to the project's root directory and run this command:
mvn spring-boot:run
- Launch the Postman app and enter the URI
http://localhost:8080/polls
and hit Send. - Because we don’t have any polls created yet, this command should result in an empty collection.
- If your application cannot run because something is occupying a port, use this command with the respective port number specified:
kill -kill `lsof -t -i tcp:8080`
- We accomplish the capability to add new polls to the
PollController
by implementing thePOST
verb functionality in acreatePoll
method:
@RequestMapping(value="/polls", method=RequestMethod.POST)
public ResponseEntity<?> createPoll(@RequestBody Poll poll) {
poll = pollRepository.save(poll);
return new ResponseEntity<>(null, HttpStatus.CREATED);
}
- Take note that the method
- has a parameter of type
@RequestBody Poll poll
@RequestBody
tells Spring that the entire request body needs to be converted to an instance of Poll
- delegates the
Poll
persistence toPollRepository
’s save methodpoll = pollRepository.save(poll);
- has a parameter of type
- Best practice is to convey the URI to the newly created resource using the Location HTTP header via Spring's
ServletUriComponentsBuilder
utility class. This will ensure that the client has some way of knowing the URI of the newly created Poll.
URI newPollUri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(poll.getId())
.toUri();
- Modify the
createPoll
method so that it returns aResponseEntity
which takes an argument of anew HttpHeaders()
whose location has been set to the abovenewPollUri
via thesetLocation
method.
- The code snippet below enables us to access an individual poll.
- The value attribute in the
@RequestMapping
takes a URI template/polls/{pollId}
. - The placeholder
{pollId}
along with@PathVarible
annotation allows Spring to examine the request URI path and extract thepollId
parameter value. - Inside the method, we use the
PollRepository
’sfindOne
finder method to read the poll and pass it as part of aResponseEntity
.
@RequestMapping(value="/polls/{pollId}", method=RequestMethod.GET)
public ResponseEntity<?> getPoll(@PathVariable Long pollId) {
Poll p = pollRepository.findOne(pollId);
return new ResponseEntity<> (p, HttpStatus.OK);
}
- The code snippet below enables us to update a poll.
@RequestMapping(value="/polls/{pollId}", method=RequestMethod.PUT)
public ResponseEntity<?> updatePoll(@RequestBody Poll poll, @PathVariable Long pollId) {
// Save the entity
Poll p = pollRepository.save(poll);
return new ResponseEntity<>(HttpStatus.OK);
}
- The code snippet below enables us to delete a poll.
@RequestMapping(value="/polls/{pollId}", method=RequestMethod.DELETE)
public ResponseEntity<?> deletePoll(@PathVariable Long pollId) {
pollRepository.delete(pollId);
return new ResponseEntity<>(HttpStatus.OK);
}
- Restart the QuickPoll application.
- Use Postman to execute a
POST
tohttp://localhost:8080/polls/
whose request body is theJSON
object below. - You can modify the request body in Postman by navigating to the
Body
tab, selecting theraw
radio button, and selecting theJSON
option from the text format dropdown.
{
"id": 1,
"question": "What's the best netflix original?",
"options": [
{ "value": "Black Mirror" },
{ "value": "Stranger Things" },
{ "value": "Orange is the New Black"},
{ "value": "The Get Down" }
]
}
- Ensure the the data has been persisted by executing a
GET
tohttp://localhost:8080/polls/1
- Upon execution, you should receive this message body.
{
"id": 1,
"question": "What's the best netflix original?",
"options": [
{
"id": 1,
"value": "Stranger Things"
},
{
"id": 2,
"value": "The Get Down"
},
{
"id": 3,
"value": "Black Mirror"
},
{
"id": 4,
"value": "Orange is the New Black"
}
]
}
- Following the principles used to create
PollController
, we implement theVoteController
class. - Below is the code for the
VoteController
class along with the functionality to create a vote. - The
VoteController
uses an injected instance ofVoteRepository
to performCRUD
operations on Vote instances.
@RestController
public class VoteController {
private VoteRepository voteRepository;
@Autowired
public VoteController(VoteRepository voteRepository) {
this.voteRepository = voteRepository;
}
@RequestMapping(value = "/polls/{pollId}/votes", method = RequestMethod.POST)
public ResponseEntity<?> createVote(@PathVariable Long pollId, @RequestBody Vote
vote) {
vote = voteRepository.save(vote);
// Set the headers for the newly created resource
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation(ServletUriComponentsBuilder.
fromCurrentRequest().path("/{id}").buildAndExpand(vote.getId()).toUri());
return new ResponseEntity<>(null, responseHeaders, HttpStatus.CREATED);
}
}
- To test the voting capabilities,
POST
a new Vote to the/polls/1/votes
endpoint with the option object expressed inJSON
below. - On successful request execution, you will see a Location response header with value http://localhost:8080/polls/1/votes/1.
{
"option": { "id": 1, "value": "Black Mirror" }
}
- The method
findAll
in theVoteRepository
retrieves all votes in a Database rather than a given poll. - To ensure we can get votes for a given poll, we must add the code below to our
VoteRepository
.
public interface VoteRepository extends CrudRepository<Vote, Long> {
@Query(value = "SELECT v.* " +
"FROM Option o, Vote v " +
"WHERE o.POLL_ID = ?1 " +
"AND v.OPTION_ID = o.OPTION_ID", nativeQuery = true)
public Iterable<Vote> findVotesByPoll(Long pollId);
}
- The custom finder method
findVotesByPoll
takes theID
of thePoll
as its parameter. - The
@Query
annotation on this method takes a native SQL query along with thenativeQuery
flag set totrue
. - At runtime, Spring Data JPA replaces the
?1
placeholder with the passed-inpollId
parameter value.
- Create a
getAllVotes
method in theVoteController
@RequestMapping(value="/polls/votes", method=RequestMethod.GET)
public Iterable<Vote> getAllVotes() {
return voteRepository.findAll();
}
- Create a
getVote
method in theVoteController
@RequestMapping(value="/polls/{pollId}/votes", method=RequestMethod.GET)
public Iterable<Vote> getVote(@PathVariable Long pollId) {
return voteRepository.findById(pollId);
}
- The final piece remaining for us is the implementation of the ComputeResult resource.
- Because we don’t have any domain objects that can directly help generate this resource representation, we implement two Data Transfer Objects or DTOs—OptionCount and VoteResult
- Create a sub package of
java
nameddtos
- The
OptionCount
DTO contains theID
of the option and a count of votes casted for that option.
public class OptionCount {
private Long optionId;
private int count;
public Long getOptionId() {
return optionId;
}
public void setOptionId(Long optionId) {
this.optionId = optionId;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
- The
VoteResult
DTO contains the total votes cast and a collection ofOptionCount
instances.
import java.util.Collection;
public class VoteResult {
private int totalVotes;
private Collection<OptionCount> results;
public int getTotalVotes() {
return totalVotes;
}
public void setTotalVotes(int totalVotes) {
this.totalVotes = totalVotes;
}
public Collection<OptionCount> getResults() {
return results;
}
public void setResults(Collection<OptionCount> results) {
this.results = results;
}
}
- Following the principles used in creating the
PollController
andVoteController
, we create a newComputeResultController
class
@RestController
public class ComputeResultController {
private VoteRepository voteRepository;
@Autowired
public ComputeResultController(VoteRepository voteRepository) {
this.voteRepository = voteRepository;
}
@RequestMapping(value = "/computeresult", method = RequestMethod.GET)
public ResponseEntity<?> computeResult(@RequestParam Long pollId) {
VoteResult voteResult = new VoteResult();
Iterable<Vote> allVotes = voteRepository.findVotesByPoll(pollId);
//TODO: Implement algorithm to count votes
return new ResponseEntity<VoteResult>(voteResult, HttpStatus.OK);
}
- We inject an instance of
VoteRepository
into the controller, which is used to retrieve votes for a given poll. - The
computeResult
method takespollId
as its parameter. - The
@RequestParam
annotation instructs Spring to retrieve thepollId
value from a HTTP query parameter. - The computed results are sent to the client using a newly created instance of
ResponseEntity
.
- Start/restart the
QuickPoll
application. - Using the earlier Postman requests, create a poll and cast votes on its options.
- Ensure a JSON file with a
status
of200
is returned by executing aGET
request ofhttp://localhost:8080/computeresult?pollId=1
via Postman
- Create a
exception
package inside ofio.zipcoder.springdemo.QuickPollApplication
- Create a
ResourceNotFoundException
class that extendsRuntimeException
. We'll use this to signal when a requested resource is not found. - Annotate the
ResourceNotFoundException
class with@ResponseStatus(HttpStatus.NOT_FOUND)
. This informs Spring that any request mapping that throws aResourceNotFoundException
should result in a404 NOT FOUND
http status. - Implement three constructors
- A no-arg constructor
- A constructor that takes a
String message
and passes it to the superclass constructor - A constructor that takes
String message
andThrowable cause
and passes both to the superclass constructor
Create a void method in PollController
called verifyPoll
that checks if a specific poll id exists and throws a ResourceNotFoundException
if not. Use this in any method that searches for or updates an existing poll (eg: Get, Put, and Delete methods).
Note: This means that trying to submit a PUT request for a resource that doesn't exist will not implicitly create it; it should throw a 404 instead.
Spring provides some built-in exception handling and error response, but we'll customize it a bit here. Create an ErrorDetail
class in a new io.zipcoder.tc_spring_poll_application.dto.error
package to hold relevant information any time an error occurs.
Fields (Don't forget to provide getters and setters):
String title
: a brief title of the error condition, eg: "Validation Failure" or "Internal Server Error"int status
: the HTTP status code for the current request; redundant but useful for client-side error handlingString detail
: A short, human-readable description of the error that may be presented to a userlong timeStamp
: the time in milliseconds when the error occurredString developerMessage
: detailed information such as exception class name or a stack trace useful for developers to debug
In this section we add custom handling for the exceptions we created before. A @ControllerAdvice
is an AOP feature that wraps a controller and adds some functionality when needed. In this case we are adding functionality only when an exception is thrown.
- Create RestExceptionHandler class annotated with
@ControllerAdvice
- Create a handler method with the header shown below
- Populate an ErrorDetail object in the method, and return a ResponseEntity containing the ErrorDetail and an HTTP
NOT_FOUND
status- Use java.util's
new Date().getTime()
for the timestamp - Provide the detail and developer messages from the
ResourceNotFoundException
- Use java.util's
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException rnfe, HttpServletRequest request) {...}
Now it's time to make sure that all objects persisted to the database actually contain valid values. Use the org.hibernate.validator.constraints.NotEmpty
and javax.validation.constraints.Size
and javax.validation.Valid
annotations for validation.
- In the
Poll
class:options
should be@Size(min=2, max = 6)
question
should be@NotEmpty
- To enforce these validations, add
@Valid
annotations to Poll objects inRequestMapping
-annotated controller methods (there should be 2)
In order to customize validation errors we'll need a class for error information. Create a ValidationError
class in io.zipcoder.tc_spring_poll_application.dto.error
with the following fields and appropriate getters and setters:
String code
String message
We also need a new field in the ErrorDetail
class to hold errors. There may be multiple validation errors associated with a request, sometimes more than one of the same type, so this field will be a collection, specifically a Map<String, List<ValidationError>> errors
field.
- add below handler to
RestExceptionHandler
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?>
handleValidationError( MethodArgumentNotValidException manve,
HttpServletRequest request){...}
In this handler we need to do the following:
- Create the ErrorDetail object (similar to before)
- Get the list of field validation errors
- For each field error, add it to the appropriate list in the ErrorDetail (see below)
- Return a
ResponseEntity
containing the error detail and the appropriate HTTP status code (400 Bad Request
)
List<FieldError> fieldErrors = manve.getBindingResult().getFieldErrors();
for(FieldError fe : fieldErrors) {
List<ValidationError> validationErrorList = errorDetail.getErrors().get(fe.getField());
if(validationErrorList == null) {
validationErrorList = new ArrayList<>();
errorDetail.getErrors().put(fe.getField(), validationErrorList);
}
ValidationError validationError = new ValidationError();
validationError.setCode(fe.getCode());
validationError.setMessage(messageSource.getMessage(fe, null));
validationErrorList.add(validationError);
}
Commonly used strings in your Java program can be removed from the source code and placed in a separate file. This is called externalizing, and is useful for allowing changes to text displayed without impacting actual program logic. One example of where this is done is in internationalization, the practice of providing multilingual support in an application, allowing users to use an application in their native language.
There are two steps needed here to externalize and standardize the validation error messages:
- Create a
messages.properties
file in thesrc/main/resources
directory with the given properties belowmessages.properties
is a key-value file stored in plain text. Your IDE may have a table-based view or show the contents as text.properties
files are a common idiom in Java applications; they contain additional information the application uses that doesn't impact the actual source code.
- Use an autowired
MessageSource
object in theRestExceptionHandler
to set the message on ValidationError objects (ie:setMessage(messageSource.getMessage(fe,null));
)- This object will be autowired (or injected) the same way your
CRUDRepository
instances are.
- This object will be autowired (or injected) the same way your
messages.properties
content:
NotEmpty.poll.question=Question is a required field
Size.poll.options=Options must be greater than {2} and less than {1}
- To optimize performance, it is important to limit the amount of data returned, especially in the case of a mobile client.
- REST services have the ability to give clients access large datasets in manageable chunks, by splitting the data into discrete pages or paging data.
- For this lab, we will approach this by implementing the page number pagination pattern.
- For example, a client wanting a blog post in page 3 of a hypothetical blog service can use a
GET
method resembling the following:http://blog.example.com/posts?page=3
- It is possible for the client to override the default page size by passing in a page-size parameter:
http://blog.example.com/posts?page=3&size=20
- Pagination-specific information includes
- total number of records
- total number of pages
- current page number
- page size
- In the above blog-scenario, one would expect a response body with pagination information closely resembling the
JSON
object below.
{
"data": [
... Blog Data
],
"totalPages": 9,
"currentPageNumber": 2,
"pageSize": 10,
"totalRecords": 90
}
- Read more about REST pagination in Spring by clicking here.
-
Create a
src/main/resource/import.sql
file with DML statements for populating the database upon bootstrap. Theimport.sql
should insert at least 15 polls, each with 3 or more options.-
Below is an example of
SQL
statements for creating a single poll with only one option.-
Poll Creation
insert into poll (poll_id, question) values (1, 'What is your favorite color?');
-
Option Creation
insert into option (option_id, option_value, poll_id) values (1, 'Red', 1);
-
-
-
Restart your application.
-
Use Postman to ensure database is populated by
import.sql
.
- Make use of Spring's built-in page number pagination support by researching
org.springframework.data.repository.PagingAndSortingRepository
. - Modify respective
Controller
methods to handlePageable
arguments. - Send a
GET
request tohttp://localhost:8080/polls?page=0&size=2
via Postman. - Ensure the response is a
JSON
object with pagination-specific information.