---
tags: firebase, spring-boot
---
# 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](https://firebase.google.com/docs/firestore/quickstart#java_1) 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](https://spring.io) 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**

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

3. Click **Finish**
*If you do not have that option, create you project from the site [Spring Initializr](https://start.spring.io/)*
## Connecting to your database
To connect to your database, you will need create a [Service Account](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances). 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>`:
```xml
<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:
```java=
//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](https://www.geeksforgeeks.org/mvc-design-pattern/) 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.
```java=
@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.
```java=
@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.
```java=
@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.
```java=
@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.
```java=
@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
```java=
@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
```java=
@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
```java=
@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](https://firebase.google.com/docs/firestore/quickstart#java_1) 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.
```java=
@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.
```java=
@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 the`DocumentReference` 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`
```java=34
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.
```java=44
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.
```java=58
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.
```java=
@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.
```java=30
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](https://firebase.google.com/docs/firestore/query-data/queries) for a full list of options.
We will use a similar where clause below, however, not with a `DocumentReference`.
```java=67
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.
```java=103
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.
```java=157
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.
```java=
@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.
```java=
@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.*
```java=
@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.
```java=
@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.
- `@Value`annotation 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.
```java=
@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)*
```java=
@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.
```java=
@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.
```java=
@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:
```json
{
"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.
```java=
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.
```java=
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 (&).
```java=
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.
```java=
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