Creating a Facebook Clone - Part XXV - Transcript.pdf
1. Creating a Facebook Clone - Part XXV
The last piece of the server code is the WebService layer which is again relatively boilerplate driven. The WebServices mostly invoke the services layer without much
imagination. That's a good thing in engineering terms: boring is good. But it doesn't make a thrilling story. So please bare with me as this will pick up pace a bit when we
connect this to the client side!
We have 3 WebService classes: UserWebService, MediaWebService & PostWebService.
2. @Controller
@RequestMapping("/user")
@RestController
public class UserWebService {
@Autowired
private UserService users;
@Autowired
private NotificationService notifications;
@ExceptionHandler(LoginException.class)
@ResponseStatus(value=HttpStatus.FORBIDDEN)
public @ResponseBody
ErrorDAO handleLoginException(LoginException e) {
return new ErrorDAO(e.getMessage(), 0);
}
@ExceptionHandler(UnirestException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
public @ResponseBody
ErrorDAO handleEmailException(UnirestException e) {
return new ErrorDAO("Error while sending an email: " + e.
UserWebService
Just like the UserService this class is pretty big and also includes some additional capabilities from the NotificationService. Despite the fact that most of the class is
boilerplate there is still a lot of it so I'll try to keep it manageable by splitting it down a bit.
The WebService is located at the /user base which means any URL would be below that
3. @Controller
@RequestMapping("/user")
@RestController
public class UserWebService {
@Autowired
private UserService users;
@Autowired
private NotificationService notifications;
@ExceptionHandler(LoginException.class)
@ResponseStatus(value=HttpStatus.FORBIDDEN)
public @ResponseBody
ErrorDAO handleLoginException(LoginException e) {
return new ErrorDAO(e.getMessage(), 0);
}
@ExceptionHandler(UnirestException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
public @ResponseBody
ErrorDAO handleEmailException(UnirestException e) {
return new ErrorDAO("Error while sending an email: " + e.
UserWebService
We map 2 services in this single service. Since notifications has just one method it made sense bringing it here
4. @Autowired
private NotificationService notifications;
@ExceptionHandler(LoginException.class)
@ResponseStatus(value=HttpStatus.FORBIDDEN)
public @ResponseBody
ErrorDAO handleLoginException(LoginException e) {
return new ErrorDAO(e.getMessage(), 0);
}
@ExceptionHandler(UnirestException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
public @ResponseBody
ErrorDAO handleEmailException(UnirestException e) {
return new ErrorDAO("Error while sending an email: " + e.
getMessage(), 0);
}
@ExceptionHandler(SignupException.class)
@ResponseStatus(value=HttpStatus.FORBIDDEN)
public @ResponseBody
ErrorDAO handleSignupException(SignupException e) {
return new ErrorDAO(e.getMessage(), 0);
}
UserWebService
The exception handlers grab one of the 3 exception types that can be thrown in one of the methods in this class and convert them to an error response object.
Notice that the error response is the ErrorDAO which generates JSON with an error message. This is the ErrorDAO class.
5. public class ErrorDAO {
private String error;
private int code;
public ErrorDAO() {
}
public ErrorDAO(String error, int code) {
this.error = error;
this.code = code;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
ErrorDAO
The Error DAO object is just a simple DAO that includes the error message and an optional error code.
6. @ExceptionHandler(SignupException.class)
@ResponseStatus(value=HttpStatus.FORBIDDEN)
public @ResponseBody
ErrorDAO handleSignupException(SignupException e) {
return new ErrorDAO(e.getMessage(), 0);
}
@RequestMapping(method=RequestMethod.POST, value="/login")
public @ResponseBody
UserDAO login(@RequestBody UserDAO u) throws LoginException {
return users.login(u.getEmail(), u.getPhone(), u.getPassword());
}
@RequestMapping(method=RequestMethod.GET, value="/refresh")
public UserDAO refreshUser(
@RequestHeader(name="auth", required=true) String auth) {
return users.refreshUser(auth);
}
@RequestMapping(method=RequestMethod.POST, value="/signup")
public @ResponseBody
UserDAO signup(@RequestBody UserDAO user)
throws SignupException, UnirestException {
return users.signup(user);
UserWebService
The user/login URL returns the response as JSON in the body and accept a JSON request in the body of the post. The exception will be converted to an ErrorDAO if it's
thrown
7. @ExceptionHandler(SignupException.class)
@ResponseStatus(value=HttpStatus.FORBIDDEN)
public @ResponseBody
ErrorDAO handleSignupException(SignupException e) {
return new ErrorDAO(e.getMessage(), 0);
}
@RequestMapping(method=RequestMethod.POST, value="/login")
public @ResponseBody
UserDAO login(@RequestBody UserDAO u) throws LoginException {
return users.login(u.getEmail(), u.getPhone(), u.getPassword());
}
@RequestMapping(method=RequestMethod.GET, value="/refresh")
public UserDAO refreshUser(
@RequestHeader(name="auth", required=true) String auth) {
return users.refreshUser(auth);
}
@RequestMapping(method=RequestMethod.POST, value="/signup")
public @ResponseBody
UserDAO signup(@RequestBody UserDAO user)
throws SignupException, UnirestException {
return users.signup(user);
UserWebService
The user/refresh method is a get operation notice that it doesn't accept an argument. It uses an HTTP header named auth for the authorization token
8. @RequestMapping(method=RequestMethod.GET, value="/refresh")
public UserDAO refreshUser(
@RequestHeader(name="auth", required=true) String auth) {
return users.refreshUser(auth);
}
@RequestMapping(method=RequestMethod.POST, value="/signup")
public @ResponseBody
UserDAO signup(@RequestBody UserDAO user)
throws SignupException, UnirestException {
return users.signup(user);
}
@RequestMapping(method=RequestMethod.GET, value="/verify")
public @ResponseBody
String verifyEmailOrPhone(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="code") String code,
@RequestParam(name="email") boolean email) {
if(users.verifyEmailOrPhone(auth, code, email)) {
return "OK";
}
return "ERROR";
}
UserWebService
Notice I use the auth header almost everywhere as it's a standard part of the request.
You will notice the code is very simple, it's declarative code that defines how the WebService will look before delegating to the underlying service class.
9. users.update(user);
return "OK";
}
@RequestMapping(value="/avatar/{id:.+}", method=RequestMethod.GET)
public ResponseEntity<byte[]> getAvatar(
@PathVariable("id") String id) {
byte[] av = users.getAvatar(id);
if(av != null) {
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).
body(av);
}
return ResponseEntity.notFound().build();
}
@RequestMapping(method=RequestMethod.GET, value="/set-avatar")
public String setAvatar(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="media", required=true) String mediaId) {
users.setAvatar(auth, mediaId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
="/send-friend-request")
UserWebService
Next we have the operations that map an avatar. This means the URL `user/avata/userId` would return the avatar JPEG directly and would work even with the browser so
we can share this URL.
10. users.update(user);
return "OK";
}
@RequestMapping(value="/avatar/{id:.+}", method=RequestMethod.GET)
public ResponseEntity<byte[]> getAvatar(
@PathVariable("id") String id) {
byte[] av = users.getAvatar(id);
if(av != null) {
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).
body(av);
}
return ResponseEntity.notFound().build();
}
@RequestMapping(method=RequestMethod.GET, value="/set-avatar")
public String setAvatar(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="media", required=true) String mediaId) {
users.setAvatar(auth, mediaId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
="/send-friend-request")
UserWebService
ResponseEntity lets us control the HTTP response including the mimetype and media content
11. users.update(user);
return "OK";
}
@RequestMapping(value="/avatar/{id:.+}", method=RequestMethod.GET)
public ResponseEntity<byte[]> getAvatar(
@PathVariable("id") String id) {
byte[] av = users.getAvatar(id);
if(av != null) {
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).
body(av);
}
return ResponseEntity.notFound().build();
}
@RequestMapping(method=RequestMethod.GET, value="/set-avatar")
public String setAvatar(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="media", required=true) String mediaId) {
users.setAvatar(auth, mediaId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
="/send-friend-request")
UserWebService
In this case we send a 404 response when the user doesn't have an avatar
12. byte[] av = users.getAvatar(id);
if(av != null) {
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).
body(av);
}
return ResponseEntity.notFound().build();
}
@RequestMapping(method=RequestMethod.GET, value="/set-avatar")
public String setAvatar(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="media", required=true) String mediaId) {
users.setAvatar(auth, mediaId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
="/send-friend-request")
public String sendFriendRequest(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="userId", required=true) String userId) {
users.sendFriendRequest(auth, userId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
UserWebService
I use the GET method to make a change in the set-avatar request. This simplifies the code as it already requires a separate operation to upload the Media object
13. @RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="media", required=true) String mediaId) {
users.setAvatar(auth, mediaId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
="/send-friend-request")
public String sendFriendRequest(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="userId", required=true) String userId) {
users.sendFriendRequest(auth, userId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value
="/accept-friend-request")
public String acceptFriendRequest(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="userId", required=true) String userId) {
users.acceptFriendRequest(auth, userId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value="/notifications")
UserWebService
The friend request methods are trivial and nearly identical. They both accept the same arguments and pass them on to the underlying service calls.
I use a get operation because I don't want to add a DAO and get into the complexities of POST for this case.
Normally a get operation is a bad idea for an operation that triggers a change. I wouldn't do this on the web since it can lead to accidental resubmission of data.
However, since my client here is an app the use of a get operation simplifies some of the syntax.
14. @RequestMapping(method=RequestMethod.GET, value
="/accept-friend-request")
public String acceptFriendRequest(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="userId", required=true) String userId) {
users.acceptFriendRequest(auth, userId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value="/notifications")
public List<NotificationDAO> listNotifications(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="page") int page,
@RequestParam(name="size") int size) {
return notifications.listNotifications(auth, page, size);
}
@RequestMapping(method=RequestMethod.POST, value="/contacts")
public String uploadContacts(
@RequestHeader(name="auth", required=true) String auth,
@RequestBody List<ShadowUserDAO> contacts) {
users.uploadContacts(auth, contacts);
return "OK";
}
}
UserWebService
Finally the last piece of the UserWebService code is the notification & contacts support. This method allows us to page through the list of notifications fetching a new
page dynamically as needed
15. @RequestMapping(method=RequestMethod.GET, value
="/accept-friend-request")
public String acceptFriendRequest(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="userId", required=true) String userId) {
users.acceptFriendRequest(auth, userId);
return "OK";
}
@RequestMapping(method=RequestMethod.GET, value="/notifications")
public List<NotificationDAO> listNotifications(
@RequestHeader(name="auth", required=true) String auth,
@RequestParam(name="page") int page,
@RequestParam(name="size") int size) {
return notifications.listNotifications(auth, page, size);
}
@RequestMapping(method=RequestMethod.POST, value="/contacts")
public String uploadContacts(
@RequestHeader(name="auth", required=true) String auth,
@RequestBody List<ShadowUserDAO> contacts) {
users.uploadContacts(auth, contacts);
return "OK";
}
}
UserWebService
We can submit a list of contacts for the shadow user through this API, notice that a List from JSON will implicitly translate to the given contact list. With that we finished
the relatively simple UserWebService.