io.zipcoder.tc_spring_poll_application
named domain
.Option
Option
class in the domain
sub-package.Option
class signature is annotated with @Entity
Option
has an id
instance variable of type Long
id
should be annotated
with
@Id
@GeneratedValue
column(field)
@Column(name = "OPTION_ID")
@Column
specified, the framework assumes the field's variable-name is the persistent property name.Option
has a value
instance variable of type String
value
should be annotated
with
@Column(name = "OPTION_VALUE")
Create a getter
and setter
for each of the respective instance variables.
Poll
Poll
class in the domain
sub-package.Poll
class signature is annotated with @Entity
Poll
has an id
instance variable of type Long
id
should be annotated
with
@Id
@GeneratedValue
Column(name = "POLL_ID")
Poll
has a question
instance variable of type String
question
should be annotated
with
@Column(name = "QUESTION")
Poll
has an options
instance variable of type Set
of Option
options
should be annotated
with
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "POLL_ID")
@OrderBy
Create a getter
and setter
for each of the respective instance variables.
Vote
Vote
class in the domain
sub-package.Vote
class signature is annotated with @Entity
Vote
has an id
instance variable of type Long
id
should be annotated
with
@Id
@GeneratedValue
Column(name = "VOTE_ID")
Vote
has a option
instance variable of type Option
option
should be annotated
with
@ManyToOne
@JoinColumn(name = "OPTION_ID")
Create a getter
and setter
for each of the respective instance variables.
findById
, findAll
, for retrieving data, and methods to persist and delete data.Repository
per domain
object.io.zipcoder.tc_spring_poll_application
named repositories
.OptionRepository
OptionRepository
interface in the repositories
subpackage.OptionRepository
is a subclass of CrudRepository<Option, Long>
PollRepository
PollRepository
interface in the repositories
subpackage.PollRepository
is a subclass of CrudRepository<Poll, Long>
VoteRepository
VoteRepository
interface in the repositories
subpackage.VoteRepository
is a subclass of CrudRepository<Vote, Long>
io.zipcoder.tc_spring_poll_application
named controller
.PollController
Create a PollController
class in the controller
sub package.
PollController
signature should be annotated
with @RestController
PollController
has a pollRepository
instance variable of type PollRepository
Create a constructor that accepts a PollRepository
argument and assigns its value to the pollRepository
member variable.
@Autowired
annotation.GET
request methodGET
request on the /polls
endpoint which provides a collection of all of the polls available in the QuickPolls application. Copy and paste this into your PollController
class.@RequestMapping(value="/polls", method= RequestMethod.GET)
public ResponseEntity<Iterable<Poll>> getAllPolls() {
Iterable<Poll> allPolls = pollRepository.findAll();
return new ResponseEntity<>(allPolls, HttpStatus.OK);
}
PollRepository
.ResponseEntity
and pass in Poll
data and the HttpStatus.OK
status value.Poll
data becomes part of the response body and OK
(code 200) becomes the response status code.start-class
tag in your pom.xml
encapsulates io.zipcoder.springdemo.QuickPollApplication
mvn spring-boot:run
http://localhost:8080/polls
and hit Send.kill -kill `lsof -t -i tcp:8080`
POST
request methodPollController
by implementing the POST
verb functionality in a createPoll
method:@RequestMapping(value="/polls", method=RequestMethod.POST)
public ResponseEntity<?> createPoll(@RequestBody Poll poll) {
poll = pollRepository.save(poll);
return new ResponseEntity<>(null, HttpStatus.CREATED);
}
@RequestBody Poll poll
@RequestBody
tells Spring that the entire request body needs to be converted to an instance of PollPoll
persistence to PollRepository
’s save method
poll = pollRepository.save(poll);
createPoll
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();
createPoll
method so that it returns a ResponseEntity
which takes an argument of a new HttpHeaders()
whose location has been set to the above newPollUri
via the setLocation
method.GET
request method@RequestMapping
takes a URI template /polls/{pollId}
.{pollId}
along with @PathVarible
annotation allows Spring to examine the request URI path and extract the pollId
parameter value.PollRepository
’s findOne
finder method to read the poll and pass it as part of a ResponseEntity
.@RequestMapping(value="/polls/{pollId}", method=RequestMethod.GET)
public ResponseEntity<?> getPoll(@PathVariable Long pollId) {
Poll p = pollRepository.findOne(pollId);
return new ResponseEntity<> (p, HttpStatus.OK);
}
UPDATE
request method@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);
}
DELETE
request method.@RequestMapping(value="/polls/{pollId}", method=RequestMethod.DELETE)
public ResponseEntity<?> deletePoll(@PathVariable Long pollId) {
pollRepository.delete(pollId);
return new ResponseEntity<>(HttpStatus.OK);
}
POST
to http://localhost:8080/polls/
whose request body is the JSON
object below.Body
tab, selecting the raw
radio button, and selecting the JSON
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" }
]
}
GET
to http://localhost:8080/polls/1
{
"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"
}
]
}
VoteController
PollController
, we implement the VoteController
class.VoteController
class along with the functionality to create a vote.VoteController
uses an injected instance of VoteRepository
to perform CRUD
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);
}
}
VoteController
POST
a new Vote to the /polls/1/votes
endpoint with the option object expressed in JSON
below.{
"option": { "id": 1, "value": "Black Mirror" }
}
VoteRepository
findAll
in the VoteRepository
retrieves all votes in a Database rather than a given poll.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);
}
findVotesByPoll
takes the ID
of the Poll
as its parameter.@Query
annotation on this method takes a native SQL query along with the nativeQuery
flag set to true
.?1
placeholder with the passed-in pollId
parameter value.VoteController
getAllVotes
method in the VoteController
@RequestMapping(value="/polls/votes", method=RequestMethod.GET)
public Iterable<Vote> getAllVotes() {
return voteRepository.findAll();
}
getVote
method in the VoteController
@RequestMapping(value="/polls/{pollId}/votes", method=RequestMethod.GET)
public Iterable<Vote> getVote(@PathVariable Long pollId) {
return voteRepository.findById(pollId);
}
java
named dtos
OptionCount
OptionCount
DTO contains the ID
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;
}
}
VoteResult
VoteResult
DTO contains the total votes cast and a collection of OptionCount
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;
}
}
ComputeResultController
PollController
and VoteController
, we create a new ComputeResultController
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);
}
VoteRepository
into the controller, which is used to retrieve votes for a given poll.computeResult
method takes pollId
as its parameter.@RequestParam
annotation instructs Spring to retrieve the pollId
value from a HTTP query parameter.ResponseEntity
.QuickPoll
application.status
of 200
is returned by executing a GET
request of http://localhost:8080/computeresult?pollId=1
via PostmanResourceNotFoundException
exception
package inside of io.zipcoder.springdemo.QuickPollApplication
ResourceNotFoundException
class that extends RuntimeException
. We'll use this to signal when a requested resource is not found.ResourceNotFoundException
class with @ResponseStatus(HttpStatus.NOT_FOUND)
. This informs Spring that any request mapping that throws a ResourceNotFoundException
should result in a 404 NOT FOUND
http status.String message
and passes it to the superclass constructorString message
and Throwable cause
and passes both to the superclass constructorCreate 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@ControllerAdvice
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.
@ControllerAdvice
NOT_FOUND
status
new Date().getTime()
for the timestampResourceNotFoundException
@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.
Poll
class:
options
should be @Size(min=2, max = 6)
question
should be @NotEmpty
@Valid
annotations to Poll objects in RequestMapping
-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.
RestExceptionHandler
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?>
handleValidationError( MethodArgumentNotValidException manve,
HttpServletRequest request){...}
In this handler we need to do the following:
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:
messages.properties
file in the src/main/resources
directory with the given properties below
messages.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.MessageSource
object in the RestExceptionHandler
to set the message on ValidationError objects (ie: setMessage(messageSource.getMessage(fe,null));
)
CRUDRepository
instances are.messages.properties
content:
NotEmpty.poll.question=Question is a required field
Size.poll.options=Options must be greater than {2} and less than {1}
GET
method resembling the following:
http://blog.example.com/posts?page=3
http://blog.example.com/posts?page=3&size=20
JSON
object below.{
"data": [
... Blog Data
],
"totalPages": 9,
"currentPageNumber": 2,
"pageSize": 10,
"totalRecords": 90
}
Create a src/main/resource/import.sql
file with DML statements for populating the database upon bootstrap. The import.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
.
org.springframework.data.repository.PagingAndSortingRepository
.Controller
methods to handle Pageable
arguments.GET
request to http://localhost:8080/polls?page=0&size=2
via Postman.JSON
object with pagination-specific information.