Try   HackMD

Blog: Creating a Spring Project & Connecting to Firebase (2233)

Most often data being sent or recieved via a webservice is stored in some type of database. For our example we will use Firebase as we have used MongoDB with Parse Server in the past.

Creating your database

We will follow this tutorial on getting started setting up our Firestore database.

Firebase is a NoSQL database, meaning it does not use the standard SQL queries that we are used to. It also does not organize data in the way we are used to. For this exercise we will have 4 collections (tables): Caetgory, User, Post, and PostComment. These collections will contain data based on the structure we created previously. Note: The User collection will contain one previously non-existent field "uid"

We will create 1 example entry for each. Firebase does not allow you to create a data schema, as such until you create an entry the collection cannot exist.

Database Collections (Tables)

Category

Field Data Type
content String
metaTitle String
slug String
title String

User

Field Data Type
uid String
username String
email String
firstName String
lastName String
intro String
middleName String
mobile String
profile String
registeredAt Timestamp
lastLogin Timestamp

Post

Field Data Type
allowComments boolean
authorId reference (User)
categoryId array of references (Category)
content String
createdAt Timestamp
published boolean
publishedAt Timestamp
slug String
summary String
tags array of strings
title String

PostComment

Field Data Type
authorId reference (User)
content String
createdAt Timestamp
published boolean
postId reference (Post)

Creating your Spring Project

Spring is a Java based framework for building full stack web applications. This includes: frontend, backend, and webservices (SOAP & REST) among other features.

We will be using Spring in different ways. We will look at using ReactJS frontend with Spring as our REST API.

Now let's create our project!

In IntelliJ:

  1. Select New Project > Spring Intializr

    Creating Spring Project

  2. Next, add the Spring dependecies. These are the libraries found in Spring that we will need. Select the following:

    • Developer Tools > Lombok
    • Web > Spring Web

Creating Spring Project

  1. Click Finish

If you do not have that option, create you project from the site Spring Initializr

Connecting to your database

To connect to your database, you will need create a Service Account. We create a Key and download the credential file. Save the credential file somewhere on your computer where you will be able to find it later.

Some of you may already have Service Accounts and simply need to create a key.

Adding the POM dependencies

Next we will add the Google Firebase Libraries to our project. In the POM file add the following into thw <dependencies>:

<dependency>
  <groupId>com.google.firebase</groupId>
  <artifactId>firebase-admin</artifactId>
  <version>9.1.1</version>
</dependency>

Connecting at Program Start

We will add code to method main that will allow the program to automatically connect to the Firebase each time we start the application.

Place your key file that you downloaded earlier in the resources folder and rename the file serviceAccountKey.json.

Next we will add the following code to main above the existing line of code:

//This line may be different based on what your project is named. Use the appropriate class name appears above ClassLoader loader = <YOUR_APP_NAME>.class.getClassLoader(); //opens the file stored in resources File file = new File(loader.getResource("serviceAccountKey.json").getFile()); //reads the data from the file FileInputStream serviceAccount = new FileInputStream(file.getAbsolutePath()); //connect to Firebase FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .build(); FirebaseApp.initializeApp(options);

Setting Up Project Strucutre

Next we will create the required packages and classes. We will be following the Model-View-Controller (MVC) design pattern. Here is a brief explanation of the pattern.

Models

Create a new package below your root package called model. This package will contain our data models, all the classes for each of the tables. Some of these classes will be abstract. We will create a class for normal use as well as use with the REST Controllers.

Category

We being with our Category collection. Create a variable for each of the values in the database. The names should be identical to those in the database. We will use a few annotations as well:

  • @Data : Allows the compiler to generate our getter and setter methods
  • @NoArgsContstructor : Allows the compiler to gnerate our no argument constructor
  • @AllArgsConstructor : Allows the compiler to generate a constructor with all the variables as arguments. They will be listed in the order you list the variables in your class
  • @DocumentId : This is a Firebase annotation that tells the interpreter that when it pulls a DocumentId from Firebase to assign it to the specified variables
  • @Nullable : Allows for that variable to be null. This is important when we use our models to read in JSON being sent to our API.

We make the categoryId nullable so when creating a new category, we are not required to supply a categoryId which we will want Firebase to generate.

@Data @NoArgsConstructor @AllArgsConstructor public class Category { @DocumentId private @Nullable String categoryId; private String content; private String title; private String metaTitle; private String slug; }

User

Next, we will create the user class. This will be similar to the Category class. We use all the same annotations. We use @Nullable in a few more places. These are all values that we don't think are absolutely required.

@Data @NoArgsConstructor @AllArgsConstructor public class User { @DocumentId private @Nullable String userId; private @Nullable String uid; private String username; private String email; private String firstName; private @Nullable String middleName; private String lastName; private String intro; private String profile; private @Nullable String mobile; private Timestamp registeredAt; private @Nullable Timestamp lastLogin; }

Post

Now, we will move on to our Post Collection. You will note that this is broken into three(3) classes: BasePost, Post, and RestPost. We are using this design pattern because we need to handle posts differently if we are querying it from the database versus creating a new post from the user provided JSON. The larger reason for this is due to our document references.

BasePost

The BasePost class is an abstract class meaning we will never create an instance of it. It is the super class of the Post and RestPost classes. We put all the common elements of these classes in this file (ie, all the variables not of time DocumentReference).

You will not that we have also explicitly added four(4) methods.

  • We have explicitly included the getters for published and allowComments because the @Data annotation does not generate getters for boolean variables.
  • We include setters for our Timestamp variables createdAt and publishedAt because when assigning these variables we anticipate being given a String version of the date/time. We then need to convert it to the Timestamp data type
    • Timestamp is a class in the Firebase library. We use a static method fromProto to convert the parsed date/time string. Note that this method throws a ParseException. We are choosing not to handle our exceptions in our service classes.
@Data @NoArgsConstructor @AllArgsConstructor public abstract class BasePost { @DocumentId protected @Nullable String postId; protected String title; protected String content; protected String summary; protected String slug; protected String metaTitle; protected boolean published; protected boolean allowComments; protected @Nullable Timestamp createdAt; protected @Nullable Timestamp publishedAt; protected ArrayList<String> tags; public void setCreatedAt(String createdAt) throws ParseException { this.createdAt = Timestamp.fromProto(Timestamps.parse(createdAt)); } public void setPublishedAt(String publishedAt) throws ParseException { this.publishedAt = Timestamp.fromProto(Timestamps.parse(publishedAt)); } public boolean getPublished() { return published; } public boolean getAllowComments() { return allowComments; } }
Post

The Post class is the POJO implementation of the Post Collection. This is the version of the class that we will use when retrieving a Post from the database. This is a serializable class (can be automatically converted to JSON). Note we do not use the @AllArgsConstructor annotation rather we write our own verion in which we call our parent constructor.

@Data @NoArgsConstructor public class Post extends BasePost{ private User authorId; private ArrayList<Category> categoryId; public Post(@Nullable String postId, String title, String content, String summary, String slug, String metaTitle, boolean published, boolean allowComments, @Nullable Timestamp createdAt, @Nullable Timestamp publishedAt, ArrayList<String> tags, User authorId, ArrayList<Category> categoryId) { super(postId,title,content,summary,slug,metaTitle,published,allowComments,createdAt,publishedAt,tags); this.authorId = authorId; this.categoryId = categoryId; } }
RestPost

The RestPost class is the implementation we will use when receiving data via an API call to create a new Post. Note the similarities and differences between the Post and RestPost classes. The main difference is the data type of authorId and categoryId. In this class, these are of type DocumentReference which is the desired data type to be stored in Firebase.

We include our own set methods. These methods expect to receive the string version of the DocumentId being referenced. Because the data type must be a DocumentReference, we use the provided string to query the database for the particular document reference. In the case of categoryId, it is an array, so we use a loop.

@Data @NoArgsConstructor public class RestPost extends BasePost{ private DocumentReference authorId; private ArrayList<DocumentReference> categoryId; public RestPost(@Nullable String postId, String title, String content, String summary, String slug, String metaTitle, boolean published, boolean allowComments, @Nullable Timestamp createdAt, @Nullable Timestamp publishedAt, ArrayList<String> tags, DocumentReference authorId, ArrayList<DocumentReference> categoryId) { super(postId,title,content,summary,slug,metaTitle,published,allowComments,createdAt,publishedAt,tags); this.authorId = authorId; this.categoryId = categoryId; } public void setAuthorId(String author) { Firestore db = FirestoreClient.getFirestore(); this.authorId = db.collection("User").document(author); } public void setCategoryId(ArrayList<String> categoryId) { Firestore db = FirestoreClient.getFirestore(); this.categoryId = new ArrayList<>(); for(String cat : categoryId) { this.categoryId.add(db.collection("Category").document(cat)); } } }

This is our first glimpse of writing a Firebase query. Let's take a look at the parts. First we have the code that gets you access to the database Firestore db = FirestoreClient.getFirestore();.

FirestoreClient provides access to Google Cloud Firestore. Use this API to obtain a Firestore instance, which provides methods for updating and querying data in Firestore.

Next we get the DocumentReference. A DocumentReference refers to a document location in a Firestore database and can be used to write, read, or listen to the location. The document at the referenced location may or may not exist.

PostComment Collection

The PostComment collection has similar requirements as the Post collection given the use of DocumentReferences. This is divided into three(3) classes: BasePostComment, PostComment, and RestPostComment.

BasePostComment
@Data @NoArgsConstructor @AllArgsConstructor public abstract class BasePostComment { @DocumentId protected @Nullable String commentId; protected String content; protected String title; protected boolean published; protected @Nullable Timestamp createdAt; protected @Nullable Timestamp publishedAt; public void setCreatedAt(String createdAt) throws ParseException { this.createdAt = Timestamp.fromProto(Timestamps.parse(createdAt)); } public void setPublishedAt(String publishedAt) throws ParseException { this.publishedAt = Timestamp.fromProto(Timestamps.parse(publishedAt)); } public boolean getPublished() { return published; } }
PostComment
@Data @NoArgsConstructor public class PostComment extends BasePostComment{ private Post postId; private User authorId; public PostComment(@Nullable String commentId, String content, String title, boolean published, Timestamp createdAt, Timestamp publishedAt, User authorId, @Nullable Post postId) { super(commentId,content,title,published,createdAt,publishedAt); this.postId = postId; this.authorId = authorId; } }
RestPostComment
@Data @NoArgsConstructor public class RestPostComment extends BasePostComment{ private DocumentReference authorId; private DocumentReference postId; public RestPostComment(@Nullable String commentId, String content, String title, boolean published, Timestamp createdAt, Timestamp publishedAt, DocumentReference authorId, DocumentReference postId) { super(commentId, content, title,published, createdAt,publishedAt); this.authorId = authorId; this.postId = postId; } public void setAuthorId(String author) { Firestore db = FirestoreClient.getFirestore(); this.authorId = db.collection("User").document(author); } public void setPostId(String post) { Firestore db = FirestoreClient.getFirestore(); this.postId = db.collection("Post").document(post); } }

Services

As mentioned on the MVC explanation, this pattern does not include the business logic. For web-based applications the business logic is often referred to as a service. As such we will be write the logic of how data in handled in our service classes. These classes will contain the logic of retrieving and sending data between the application and Firebase, as well as any data manipulation required.

Service Components are the class file which contains @Service annotation. These class files are used to write business logic in a different layer, separated from @RestController class file.

Create a package under the root package named service. This package should be on the same level as pacakge model. We will create a service for each of models. Create four (4) service classes CategoryService, UserService, PostService, and PostCommentService.

Please refer to the Firebase Documentation for help writing queries.

CategoryService

Our CategoryService will include a single method that allows us to get all the categories from the database. Given that we are expecting more than 1 result, we will use the Query class. The Query class (and its subclass, DatabaseReference) are used for reading data. We will return an ArrayList of Category objects.

@Service public class CategoryService { private Firestore db = FirestoreClient.getFirestore(); public ArrayList<Category> getCategories() throws ExecutionException, InterruptedException { Query query = db.collection("Category").orderBy("title", Query.Direction.ASCENDING); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); ArrayList<Category> categories = documents.size() > 0 ? new ArrayList<>() : null; for(QueryDocumentSnapshot doc : documents){ categories.add(doc.toObject(Category.class)); } return categories; } }

We want to return the categories in alphabetical order, so we order it by title orderBy("title", Query.Direction.ASCENDING)

We want to make our calls to the database asynchronous, meaning we don't wait for it to return the answe befor we start a new process. To this, we use the ApiFuture class. A Future represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation. Making the calls could cause an exception; we are simply re-throwing the exceptions. We will handle them in our controllers.

This particular Future request return a QuerySnapshot. A QuerySnapshot contains zero or more DocumentSnapshot objects representing the results of a query. The documents can be accessed as an array via the docs property or enumerated using the forEach method. The number of documents can be determined via the empty and size properties. A DocumentSnapshot contains data read from a document in your Cloud Firestore database. The data can be extracted with the getData or get methods.
A QueryDocumentSnapshot offers the same API surface as a DocumentSnapshot. Since query results contain only existing documents, the exists property will always be true and data() will never return 'undefined'.

We then iterate over the list of documents and convert each to being a Cateogry object using the toObject method. Each is added to the ArrayList.

UserService

Next is our UserService. This service will include the ability to get all the users, get a specific user, create, update and delete a user.

@Service public class UserService { private Firestore db = FirestoreClient.getFirestore(); //used to log information in the console while testing private final Log logger = LogFactory.getLog(this.getClass()); public ArrayList<User> getUsers() throws ExecutionException, InterruptedException { Query query = db.collection("User"); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); ArrayList<User> users = documents.size() > 0 ? new ArrayList<>() : null; for(QueryDocumentSnapshot doc : documents) { users.add(doc.toObject(User.class)); } return users; } public User getUser(String userId) throws ExecutionException, InterruptedException { User user = null; DocumentReference doc = db.collection("User").document(userId); ApiFuture<DocumentSnapshot> future = doc.get(); user = future.get().toObject(User.class); return user; }

When retrieving a single object, we use theDocumentReference class we saw earlier. We pass the userId as a parameter to the document method. Notice the ApiFuture in this instance as a type of DocumentSnapshot rather than QueryDocumentSnapshot

public String createUser(User user) throws ExecutionException, InterruptedException { String userId = null; user.setRegisteredAt(Timestamp.now()); ApiFuture<DocumentReference> future = db.collection("User").add(user); DocumentReference userRef = future.get(); userId = userRef.getId(); return userId; }

When creating a user, we use the POJO User class. We set the registrerdAt by creating a current Timestamp. We still make the call asynchronously using the add method. Finally we return the new DocumentId that was generated.

public void updateUser(String id, Map<String, String> updateValues){ String [] allowed = {"firstName", "lastName", "intro", "middleName", "mobile", "profile"}; List<String> list = Arrays.asList(allowed); Map<String, Object> formattedValues = new HashMap<>(); for(Map.Entry<String, String> entry : updateValues.entrySet()) { String key = entry.getKey(); if(list.contains(key)) formattedValues.put(key, entry.getValue()); } DocumentReference userDoc = db.collection("User").document(id); userDoc.update(formattedValues); }

The update method only allows the user to update specific fields. This is managed by the allowed array. To process the update, we create a HashMap of the key/value pairs we wish to updated. Once again we do this asynchronously. This is WriteResult class. This class returns a timestamp of the update.

public User getUser(DocumentReference userRef) throws ExecutionException, InterruptedException { ApiFuture<DocumentSnapshot> userQuery = userRef.get(); DocumentSnapshot userDoc = userQuery.get(); return userDoc.toObject(User.class); } }

This version of the getUser method accepts a DocumentReference. It is for internal use of the program.

PostService

Next we will look at our PostService. This is our largest services. We will be able to get all posts, get a post by user id, get all posts in a category, get a post by the post id and create, update, and delete a post.

@Service public class PostService { protected final Log logger = LogFactory.getLog(this.getClass()); private Firestore db = FirestoreClient.getFirestore(); private Post getPost(DocumentSnapshot doc) throws ExecutionException, InterruptedException { UserService userService = new UserService(); User user = userService.getUser((DocumentReference) doc.get("authorId") ); ArrayList<Category> categories = new ArrayList<>(); ArrayList<DocumentReference> refs = (ArrayList<DocumentReference>) doc.get("categoryId"); for(DocumentReference ref : refs) { ApiFuture<DocumentSnapshot> catQuery = ref.get(); DocumentSnapshot catDoc = catQuery.get(); Category category = catDoc.toObject(Category.class); categories.add(category); } //logger.info(categories); return new Post(doc.getId(),doc.getString("title"), doc.getString("content"), doc.getString("summary"), doc.getString("slug"), doc.getString("metaTitle"), doc.getBoolean("published"), doc.getBoolean("allowComments"), doc.getTimestamp("createdAt"), doc.getTimestamp("publishedAt"), (ArrayList<String>) doc.get("tags"), user, categories); }

Our first method is private. This method is used by several of the methods below. Creating serializable Post object, requires running two(2) additional queries because of the document references. This method allows us not need to write repeatedly. Because of the nature of the Post, we cannot use the toObject method we have previously, so we are creating the object using the constructor.

The first use of the private method is seen below in the getPosts method.

public ArrayList<Post> getPosts() throws ExecutionException, InterruptedException { Query query = db.collection("Post"); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); ArrayList<Post> posts = (documents.size() > 0) ? new ArrayList<>() : null; for(QueryDocumentSnapshot doc : documents) { posts.add(getPost(doc)); } return posts; } public ArrayList<Post> getPostsByUser(String userId) throws ExecutionException, InterruptedException { DocumentReference userRef = db.collection("User").document(userId); Query query = db.collection("Post") .whereEqualTo("authorId", userRef); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); ArrayList<Post> posts = documents.size() > 0 ? new ArrayList<>() : null; for(QueryDocumentSnapshot doc : documents) { posts.add(getPost(doc)); } return posts; }

This is our first query using a where clause. In this instance we want the records based on a particular user document reference. Because we are comparing document references, we need to first get a DocumentReference of the specified user. Again, please refer to the Firebase Documentation for a full list of options.

We will use a similar where clause below, however, not with a DocumentReference.

public ArrayList<Post> getPostsByCategory(String category) throws ExecutionException, InterruptedException { ArrayList<Post> posts = null; DocumentReference catRef = null; ApiFuture<QuerySnapshot> snap = db.collection("Category") .whereEqualTo("slug", category).get(); List<QueryDocumentSnapshot> catDocs = snap.get().getDocuments(); if(catDocs.size() > 0) catRef = catDocs.get(0).getReference(); if(catRef != null) { Query query = db.collection("Post") .whereArrayContains("categoryId", catRef); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); posts = documents.size() > 0 ? new ArrayList<>() : null; for (QueryDocumentSnapshot doc : documents) { posts.add(getPost(doc)); } } return posts; } public Post getPostById(String postId) throws ExecutionException, InterruptedException { Post post = null; DocumentReference postDoc = db.collection("Post").document(postId); ApiFuture<DocumentSnapshot> future = postDoc.get(); DocumentSnapshot doc = future.get(); if(doc.exists()) post = getPost(doc); return post; }

As was noted when we created our models, we use the RestPost class when creating new posts. This allows us to simplify this method, as seen below.

public String createPost(RestPost post) throws ExecutionException, InterruptedException { String postId = null; ApiFuture<DocumentReference> future = db.collection("Post").add(post); DocumentReference postRef = future.get(); postId = postRef.getId(); return postId; } public void updatePost(String id, Map<String, Object> updateValues) throws ParseException { String [] allowed = {"allowComments", "categoryId", "content", "slug", "summary", "tags", "metaTitle", "title", "published", "publishedAt"}; List<String> list = Arrays.asList(allowed); Map<String, Object> formattedValues = new HashMap<>(); for(Map.Entry<String, Object> entry : updateValues.entrySet()) { String key = entry.getKey(); if(list.contains(key)) { switch(key) { case "publishedAt": formattedValues.put(key, Timestamp.fromProto(Timestamps.parse((String) entry.getValue()))); break; case "categoryId": ArrayList<DocumentReference> catRefs = new ArrayList<>(); ArrayList<String> categories = (ArrayList<String>) entry.getValue(); for(String cat : categories) { DocumentReference catRef = db.collection("Category").document(cat); if(catRef != null) catRefs.add(catRef); } if(catRefs.size() > 0) formattedValues.put(key, catRefs); break; default: formattedValues.put(key, entry.getValue()); break; } } } //add update statement } public void deletePost(String postId){ DocumentReference postDoc = db.collection("Post").document(postId); postDoc.delete(); }

This version of getPost is used internally in other services. This allows for us to pass a DocumentReference and fetch the Post object. Note that is uses the private getPost created at the start of the class.

public Post getPost(DocumentReference postRef) throws ExecutionException, InterruptedException { DocumentSnapshot doc = postRef.get().get(); return getPost(doc); } }

PostCommentService

Finally, we have the PostCommentService. This service allows you get all comments for a specific post, create a comment, and delete a comment.

@Service public class PostCommentService { private Firestore db = FirestoreClient.getFirestore(); public ArrayList<PostComment> getComments(String postId) throws ExecutionException, InterruptedException { UserService userService = new UserService(); PostService postService = new PostService(); DocumentReference postDoc = db.collection("Post").document(postId); Query query = db.collection("PostComment") .whereEqualTo("postId", postDoc); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); ArrayList<Comment> comments = documents.size() > 0 ? new ArrayList<>() : null; for(QueryDocumentSnapshot doc : documents) { User user = userService.getUser((DocumentReference) doc.get("authorId")); Post post = postService.getPost(postDoc); comments.add(new Comment(doc.getId(),doc.getString("content"), doc.getString("title"), doc.getBoolean("published"), doc.getTimestamp("createdAt"), doc.getTimestamp("publishedAt"), user, post)); } return comments; } public String createComment(RestPostComment comment) throws ExecutionException, InterruptedException { String commentId = null; ApiFuture<DocumentReference> future = db.collection("PostComment").add(comment); DocumentReference postRef = future.get(); commentId = postRef.getId(); return commentId; } public void deleteComment(String commentId){ DocumentReference commentDoc = db.collection("PostComment").document(commentId); commentDoc.delete(); } }

Utilities

Before we dive into the final part of our MVC, controllers, we will create a few utility classes. These are classes that we will use to create a standard format for our errors and API responses.

ErrorMessage

The ErrorMessge class defines the required pieces of error message our program will return.

@Data @NoArgsConstructor @AllArgsConstructor public class ErrorMessage { private String message, className, stackTrace; }

It is a basic class that requires a message, a className , and a stackTrace.

ResponseWrapper

This class is the basic format for all our API responses. We will use the ResponseEntity class. ResponseEntity represents the whole HTTP response: status code, headers, and body. As a result, we can use it to fully configure the HTTP response. ResponseEntity is a generic type. Consequently, we can use any type as the response body. We are returning a Map (hash map) which can include any type of Java object. Remember, all classes in Java inherit from class Object.

@Data @AllArgsConstructor public class ResponseWrapper { private int statusCode; private String name; private Object payload; public ResponseEntity getResponse() { return ResponseEntity.status(statusCode).body(Collections.singletonMap(name, payload)); } }

Note that we do no include a no-argument constructor.

WebConfig

Our final utility is the WebConfig class. This is going to allow us to send request from the same URL (aka CORS). We will learn more about this, and expand this class when we learn to secure our API. For now we will keep it simple.

@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**"); } }

Controllers

Final step is to create our controllers. This is where we will put our resource (endpoint) definitions. Create a packagage called controller. Create the following classes in that package CategoryController, UserController , PostController and PostCommentController.

Before we begin, in your application.properties file, add the following:

response.name=error
response.status=500

These are our default values.

CategoryController

The CategoryController will make our CategoryService getCategoies method available via the API. We will use a few annotations:

  • @RestController indicates that the data returned by each method will be written straight into the response body instead of rendering a template.
  • @RequestMapping annotation for mapping web requests onto methods in request-handling classes with flexible method signatures.
  • @Valueannotation is used to assign default values to variables and method arguments. We can read spring environment variables as well as system variables.
  • @GetMapping annotation for mapping HTTP GET requests onto specific handler methods.
@RestController //identified this class a controller used for REST API class. @RequestMapping("/api/category")//sets up the base resource url for all calls to methods in this file public class CategoryController { CategoryService categoryService; @Value("${response.status}") private int statusCode; @Value("${response.name}") private String name; private Object payload; private ResponseWrapper response; private static final String CLASS_NAME = "CategoryService"; public CategoryController(CategoryService categoryService) { this.categoryService = categoryService; payload = null; } @GetMapping("/") public ResponseEntity<Map<String,Object>> getCategories(){ try{ payload = categoryService.getCategories(); statusCode = 200; name = "categories"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch categories from database.",CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } }

We create class level variables for our service, the HTTP status code, the name, payload, and the ResponseWrapper class. Also the class name is a static and final variable.

Our getCategories method is found at the base path of the resource URI. It returns a ResponseEntity as previously introduced. Here you can see we are using try/catch statements. As mentioned in the model and service sections, we will handle potential exceptions here. As such, the catch statement will handle the occurance of any of these exceptions. If an exception occurs, an ErrorMessage object is created and returned as the payload. If successful, the result of the query is returned as the payload, and status code is set to 200.

UserController

Our UserController will expose the UserServices methods. Note that the controller starts with similar variables and constructor as the previous controller. All of our controllers will look this way. We introduce a few new annotations:

  • @PathVariable an be used to handle template variables in the request URI mapping, and set them as method parameters.
  • @RequestBody annotation maps the HttpRequest body to a transfer or domain object, enabling automatic deserialization of the inbound HttpRequest body onto a Java object.
  • @PostMapping annotation for mapping HTTP POST requests onto specific handler methods.
  • @PutMapping annotation for mapping HTTP PUT requests onto specific handler methods.

This is also the first time we introduce use of path variable. Path variables, as you may recall, are values that we want to pass to our resouce (eg. user id). In the mapping annotation, the variable name is placed in curly braces to denote that is is a variable and not a string literal. In front of the corresponding method parameter use the @PathVariable annotation including the name property.The name must match the value in the mapping. (See lines 34-35 for an example)

@RestController @RequestMapping("/api/user") public class UserController { UserService userService; @Value("${response.status}") private int statusCode; @Value("${response.name}") private String name; private Object payload; private ResponseWrapper response; private static final String CLASS_NAME = "UserService"; public UserController(UserService userService) { this.userService = userService; payload = null; } @GetMapping("/") public ResponseEntity<Map<String, Object>> getAllUsers(){ try{ payload = userService.getUsers(); statusCode = 200; name = "users"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch users from database", CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @GetMapping("/{userId}") public ResponseEntity<Map<String,Object>> getUserById(@PathVariable(name="userId") String id){ try{ payload = userService.getUser(id); statusCode = 200; name = "user"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch user with id " + id + " from database.",CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @PostMapping("/") public ResponseEntity<Map<String,Object>> createUser(@RequestBody User user){ try{ payload = userService.createUser(user); statusCode = 201; name = "userId"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot create new user in database.", CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @PutMapping("/{userId}") public ResponseEntity<Map<String,Object>> updateUser(@PathVariable(name="userId") String id, @RequestBody Map<String, String> updateValues){ try{ userService.updateUser(id, updateValues); statusCode = 201; name = "message"; payload = "Update successful for user with id " + id; } catch (Exception e) { payload = new ErrorMessage("Cannot update user with id " + id,CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } }

RequestBody values are expected to be in JSON format.

PostController

The PostController exposes the PostService methods. We add another new annotation:

  • @DeleteMapping annotation for mapping HTTP DELETE requests onto specific handler methods.
@RestController @RequestMapping("/api/post") public class PostController { private PostService postService; @Value("${response.status}") private int statusCode; @Value("${response.name}") private String name; private Object payload; private ResponseWrapper response; private static final String CLASS_NAME = "PostService"; public PostController(PostService postService) { this.postService = postService; payload = null; } @GetMapping("/") public ResponseEntity<Map<String, Object>> getAllPosts(){ try { payload = postService.getPosts(); statusCode = 200; name = "posts"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch posts from database", CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @GetMapping("/user/{userId}") public ResponseEntity<Map<String,Object>> getPostByUser(@PathVariable(name="userId") String id){ try{ payload = postService.getPostsByUser(id); statusCode = 200; name = "posts"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch posts for user " + id + " from database.",CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @GetMapping("/{postId}") public ResponseEntity<Map<String,Object>> getPostById(@PathVariable(name="postId") String id){ try{ payload = postService.getPostById(id); statusCode = 200; name = "post"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch post with id " + id + " from database.",CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @GetMapping("/c/{catId}") public ResponseEntity<Map<String,Object>> getPostByCategory(@PathVariable(name="catId") String id){ try{ payload = postService.getPostsByCategory(id); statusCode = 200; name = "posts"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch posts for category " + id + " from database.",CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @PostMapping("/") public ResponseEntity<Map<String,Object>> createPost(@RequestBody RestPost post){ try{ payload = postService.createPost(post); statusCode = 201; name = "postId"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot create new post in database.", CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @PutMapping("/{postId}") public ResponseEntity<Map<String,Object>> updatePost(@PathVariable(name="postId") String id, @RequestBody Map<String, Object> updateValues){ try{ postService.updatePost(id, updateValues); statusCode = 201; name = "message"; payload = "Update successful for post with id " + id; }catch (ParseException e){ statusCode = 400; payload = new ErrorMessage("Cannot parse JSON",CLASS_NAME, e.toString()); } catch (Exception e) { payload = new ErrorMessage("Cannot update post with id " + id,CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @DeleteMapping("/{postId}") public ResponseEntity<Map<String,Object>> removePost(@PathVariable(name="postId") String id){ try{ postService.deletePost(id); statusCode = 204; name = "message"; payload = "Delete successful for post with id " + id; }catch (Exception e){ payload = new ErrorMessage("Cannot delete post with id " + id, CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } }

PostCommentController

Finally, the PostCommentController exposes the methods of the PostCommentService.

@RestController @RequestMapping("/api/comment") public class PostCommentController { PostCommentService postCommentService; @Value("${response.status}") private int statusCode; @Value("${response.name}") private String name; private Object payload; private ResponseWrapper response; private static final String CLASS_NAME = "CommentService"; public PostCommentController(PostCommentService postCommentService) { this.postCommentService = postCommentService; payload = null; } @GetMapping("/{postId}") public ResponseEntity<Map<String,Object>> getCommentByPostId(@PathVariable(name="postId") String id){ try{ payload = postCommentService.getComments(id); statusCode = 200; name = "comments"; } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Cannot fetch comments for post with id " + id + " from database.",CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @DeleteMapping("/{commentId}") public ResponseEntity<Map<String,Object>> removeComment(@PathVariable(name="commentId") String id){ try{ postCommentService.deleteComment(id); statusCode = 204; name = "message"; payload = "Delete successful for comment with id " + id; }catch (Exception e){ payload = new ErrorMessage("Cannot delete comment with id " + id, CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } @PostMapping("/") public ResponseEntity<Map<String,Object>> createComment(@RequestBody RestComment comment){ try{ payload = postCommentService.createComment(comment); statusCode = 201; name = "commentId"; } catch (Exception e) { payload = new ErrorMessage("Cannot create comment in database" ,CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode,name, payload); return response.getResponse(); } }

Testing

Open Postman and create a new request. In the request URL area enter http://localhost:8080/api/category/. If you have a different value for the RequestMapping or the GetMapping in your CategoryController, enter what you have there.

If all works correctly, you should be seeing a json list of results from your database. I should look something like this:

{
    "categories": [
        {
            "categoryId": "0k2RCIWDdzHTddMESuJq",
            "content": "Latest business new",
            "title": "Business",
            "metaTitle": "business",
            "slug": "business"
        },
        {
            "categoryId": "JzQpcgIf9tDGv176Z8wp",
            "content": "The latest in local and national sports coveage and comentary",
            "title": "Sports",
            "metaTitle": "sports",
            "slug": "sports"
        },
        {
            "categoryId": "8fsziJ4dA3b9h23cAT0s",
            "content": "Update to the minute tech news.",
            "title": "Technology",
            "metaTitle": "tech",
            "slug": "tech"
        }
    ]
}

Sorting & Filtering

You can add additional sort and filter values. We will alter the getPosts method in the PostService and the asssociated controller method.

We will add the following abilities:

  • Chose what field to sort by
  • Sort ascending or descending
  • Limit the number of results returned

Here is a sample request URI including out sort and filter query parameters:

http://localhost:8080/api/post/?sort=des&limit=10&by=createdAt

Edit PostService

We will add a variable as a parameter for each option to the getPosts method.

public ArrayList<Post> getPosts( int limit, String sortBy, String sort ) throws ExecutionException, InterruptedException

We will then add the code to sort or filter for each option. This should follow your Query query = db.collection("Post"); line.

if(limit > 0) query = query.limit(limit); if(!sortBy.equals("")) query = query.orderBy(sortBy, (Objects.equals(sort, "asc") ? Query.Direction.ASCENDING : Query.Direction.DESCENDING));

Each time you apply a sort/limit/where clause, the method returns a new Query object. Be sure to reassign your variable each time as seen above.

Edit PostController

Now in your post controller, we need to receive these variables and pass them to the the service. We will receive them as query parameters. If you recall from above, a query parameter is added to end of a URL following a question mark(?) and is joined by ampersands (&).

public ResponseEntity<Map<String, Object>> getAllPosts( @RequestParam(name="limit", required = false, defaultValue = "0") int limit, @RequestParam(name = "by" , required = false, defaultValue = "") String sortBy, @RequestParam(name = "sort" , required = false, defaultValue = "asc") String sort)

In the above code we added a new annotation @RequestParam. It is used to extract query parameters, form parameters, and even files from the request. In this example we are using to retrieve our query parameters. This annotations include several variables that can be set. We use the following:

  • name : name of the query parameter. It should match what you type in the URI.
  • required : boolean value as to whether or not the value must always be passed
  • defaultValue : the value used when none is passed by the user

Note that if a parameter is an ArrayList, the @RequestParam annotation will automatically convert comma separated lists into any List type include an array.

Next we edit the method call.

payload = postService.getPosts(limit, sortBy, sort);

Test Your Filter and Sort

Now test that the filter and sort work by adding query parameters to your Postman call. You can enter them in the table that appears below the URL