- 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_applicationnameddomain.
-
Create an
Optionclass in thedomainsub-package. -
Optionclass signature is annotated with@Entity -
Optionhas anidinstance variable of typeLongidshould beannotatedwith@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
@Columnspecified, the framework assumes the field's variable-name is the persistent property name.
-
Optionhas avalueinstance variable of typeStringvalueshould beannotatedwith@Column(name = "OPTION_VALUE")
-
Create a
getterandsetterfor each of the respective instance variables.
-
Create a
Pollclass in thedomainsub-package. -
Pollclass signature is annotated with@Entity -
Pollhas anidinstance variable of typeLongidshould beannotatedwith@Id@GeneratedValueColumn(name = "POLL_ID")
-
Pollhas aquestioninstance variable of typeStringquestionshould beannotatedwith@Column(name = "QUESTION")
-
Pollhas anoptionsinstance variable of typeSetofOptionoptionsshould beannotatedwith@OneToMany(cascade = CascadeType.ALL)@JoinColumn(name = "POLL_ID")@OrderBy
-
Create a
getterandsetterfor each of the respective instance variables.
-
Create a
Voteclass in thedomainsub-package. -
Voteclass signature is annotated with@Entity -
Votehas anidinstance variable of typeLongidshould beannotatedwith@Id@GeneratedValueColumn(name = "VOTE_ID")
-
Votehas aoptioninstance variable of typeOptionoptionshould beannotatedwith@ManyToOne@JoinColumn(name = "OPTION_ID")
-
Create a
getterandsetterfor 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
Repositoryperdomainobject. - Create a sub-package of
io.zipcoder.tc_spring_poll_applicationnamedrepositories.
- Create an
OptionRepositoryinterface in therepositoriessubpackage. OptionRepositoryis a subclass ofCrudRepository<Option, Long>
- Create a
PollRepositoryinterface in therepositoriessubpackage. PollRepositoryis a subclass ofCrudRepository<Poll, Long>
- Create a
VoteRepositoryinterface in therepositoriessubpackage. VoteRepositoryis 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_applicationnamedcontroller.
-
Create a
PollControllerclass in thecontrollersub package.PollControllersignature should beannotatedwith@RestController
-
PollControllerhas apollRepositoryinstance variable of typePollRepositorypollRepositoryshould beannotatedwith@Inject
- The method definition below supplies a
GETrequest on the/pollsendpoint which provides a collection of all of the polls available in the QuickPolls application. Copy and paste this into yourPollControllerclass.
@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
ResponseEntityand pass inPolldata and theHttpStatus.OKstatus value. - The
Polldata becomes part of the response body andOK(code 200) becomes the response status code.
- Ensure that the
start-classtag in yourpom.xmlencapsulatesio.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/pollsand 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
PollControllerby implementing thePOSTverb functionality in acreatePollmethod:
@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@RequestBodytells Spring that the entire request body needs to be converted to an instance of Poll
- delegates the
Pollpersistence 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
ServletUriComponentsBuilderutility 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
createPollmethod so that it returns aResponseEntitywhich takes an argument of anew HttpHeaders()whose location has been set to the abovenewPollUrivia thesetLocationmethod.
- The code snippet below enables us to access an individual poll.
- The value attribute in the
@RequestMappingtakes a URI template/polls/{pollId}. - The placeholder
{pollId}along with@PathVaribleannotation allows Spring to examine the request URI path and extract thepollIdparameter value. - Inside the method, we use the
PollRepository’sfindOnefinder 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
POSTtohttp://localhost:8080/polls/whose request body is theJSONobject below. - You can modify the request body in Postman by navigating to the
Bodytab, selecting therawradio button, and selecting theJSONoption 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
GETtohttp://localhost:8080/polls/1 - Upon execution, you should receive this message body.
{
"id": 1,
"question": "What's the best netflix original?",
"options": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
},
{
"id": 4
}
]
}- Following the principles used to create
PollController, we implement theVoteControllerclass. - Below is the code for the
VoteControllerclass along with the functionality to create a vote. - The
VoteControlleruses an injected instance ofVoteRepositoryto performCRUDoperations on Vote instances.
@RestController
public class VoteController {
@Inject
private 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,
POSTa new Vote to the/polls/1/votesendpoint with the option object expressed inJSONbelow. - 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
findAllin theVoteRepositoryretrieves 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
findVotesByPolltakes theIDof thePollas its parameter. - The
@Queryannotation on this method takes a native SQL query along with thenativeQueryflag set totrue. - At runtime, Spring Data JPA replaces the
?1placeholder with the passed-inpollIdparameter value.
- Create a
getAllVotesmethod in theVoteController
@RequestMapping(value="/polls/votes", method=RequestMethod.GET)
public Iterable<Vote> getAllVotes() {
return voteRepository.findAll();
}- Create a
getVotemethod 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
javanameddtos
- The
OptionCountDTO contains theIDof 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
VoteResultDTO contains the total votes cast and a collection ofOptionCountinstances.
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
PollControllerandVoteController, we create a newComputeResultControllerclass
@RestController
public class ComputeResultController {
@Inject
private 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
VoteRepositoryinto the controller, which is used to retrieve votes for a given poll. - The
computeResultmethod takespollIdas its parameter. - The
@RequestParamannotation instructs Spring to retrieve thepollIdvalue from a HTTP query parameter. - The computed results are sent to the client using a newly created instance of
ResponseEntity.
- Start/restart the
QuickPollapplication. - Using the earlier Postman requests, create a poll and cast votes on its options.
- Ensure a JSON file with a
statusof200is returned by executing aGETrequest ofhttp://localhost:8080/computeresult?pollId=1via Postman
- Create a
exceptionpackage inside ofio.zipcoder.springdemo.QuickPollApplication - Create a
ResourceNotFoundExceptionclass that extendsRuntimeException. We'll use this to signal when a requested resource is not found. - Annotate the
ResourceNotFoundExceptionclass with@ResponseStatus(HttpStatus.NOT_FOUND). This informs Spring that any request mapping that throws aResourceNotFoundExceptionshould result in a404 NOT FOUNDhttp status. - Implement three constructors
- A no-arg constructor
- A constructor that takes a
String messageand passes it to the superclass constructor - A constructor that takes
String messageandThrowable causeand 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_FOUNDstatus- 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
Pollclass:optionsshould be@Size(min=2, max = 6)questionshould be@NotEmpty
- To enforce these validations, add
@Validannotations 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 codeString 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
ResponseEntitycontaining 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.propertiesfile in thesrc/main/resourcesdirectory with the given properties belowmessages.propertiesis a key-value file stored in plain text. Your IDE may have a table-based view or show the contents as text.propertiesfiles 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
MessageSourceobject in theRestExceptionHandlerto set the message on ValidationError objects (ie:setMessage(messageSource.getMessage(fe,null));)- This object will be autowired (or injected) the same way your
CRUDRepositoryinstances 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
GETmethod 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
JSONobject 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.sqlfile with DML statements for populating the database upon bootstrap. Theimport.sqlshould insert at least 15 polls, each with 3 or more options.-
Below is an example of
SQLstatements 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
Controllermethods to handlePageablearguments. - Send a
GETrequest tohttp://localhost:8080/polls?page=0&size=2via Postman. - Ensure the response is a
JSONobject with pagination-specific information.