--- tags: firebase,authentication,react,spring-boot,spring24 --- # Task Manager (Spring/Firebase) 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 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. Initially we will use all it's capabilities and in the second version look at using ReactJS frontend with Spring. Now let's create our project! In IntelliJ: 1. Select **New Project > Spring Intializr** ![Creating Spring Project](https://www.dropbox.com/s/az8c5na14i2p7qx/spring-set-up.PNG?dl=1) 2. Next, add the Spring dependecies. These are the libraries found in Spring that we will need. Select the following: - Developer Tools > Lombok - Developer Tools > SpringBoot DevTools - Web > Spring Web ![Creating Spring Project](https://www.dropbox.com/s/qjznmjp9y2sq40a/spring-dependencies.PNG?dl=1) 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.2.0</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 = TaskManagerApplication.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(); if(FirebaseApp.getApps().isEmpty()) 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. ### Utility Classes Before we move on to the controllers, we will create a utility class that is the wrapper for all our API responses. Create a package called `util`. This should be on the same level as `model`. Create a class named `Utility` inside the `util` package. ```java= public class Utility { // Method to retrieve DocumentReference based on the provided ID public static DocumentReference retrieveDocumentReference(String collection, String id) { Firestore db = FirestoreClient.getFirestore(); return db.collection(collection).document(id); } } ``` Now create a new Java `record`. This class will be a record. Name the class `ApiResponse`. ![Java record class](https://www.dropbox.com/scl/fi/wy2cjwyknmus75w719zdx/record-instructions.PNG?rlkey=q2mt89qo7sdtueag8ofcvheza&dl=1) ```java= public record ApiResponse(boolean success, String message, Object data, Object error) {} ``` <details> <summary>More about records</summary> In Java, both records and classes are used to define types and represent data, but they have distinct purposes and features. Let's explore the differences between records and classes and when to use each. ### Classes in Java: 1. **Complex Behavior**: Classes are primarily used to model complex entities with behavior (methods) and state (fields). 2. **Mutability**: Fields in classes can be mutable (can change after creation) unless marked as `final`. You typically have setter methods to modify the state. 3. **Inheritance and Polymorphism**: Classes can extend other classes and implement interfaces, enabling inheritance and polymorphism. 4. **Custom Equality and Hashing**: You need to override `equals()` and `hashCode()` for custom equality comparison. 5. **Custom String Representation**: You often override `toString()` for custom string representation. 6. **Builder Pattern**: Often, when dealing with complex object creation, a builder pattern or other creational patterns are used. ### Records in Java (Introduced in Java 14): 1. **Simplified Data Carriers**: Records are designed to model data-carrying objects in a more succinct manner. They primarily represent data and provide an easy way to create immutable data containers. 2. **Immutability by Default**: Fields in records are `final` and automatically become part of the constructor, promoting immutability. 3. **Concise Syntax**: Records have a compact syntax for defining fields and automatically generate `equals()`, `hashCode()`, and `toString()` methods based on the fields. 4. **No Inheritance of State**: Records can't extend other classes; they implicitly extend `java.lang.Record` and cannot declare instance variables that are not part of the record components. 5. **Use Cases**: Ideal for representing data transfer objects (DTOs), data containers, or simple data-holding structures where the focus is on data representation rather than complex behavior. ### When to Use Records vs. Classes: - **Use Records When**: - You need a simple data carrier, especially when the main purpose is data representation. - Immutability and straightforward equality comparison (based on data) are desired. - You want to keep the code compact and more readable, especially for DTOs, APIs, or data transfer purposes. - There is no need for complex behavior, inheritance, or polymorphism. - **Use Classes When**: - You need to model entities with complex behavior, state, and potential inheritance. - Mutable state and behavior modification through methods (setter methods) are required. - You need to implement interfaces, extend classes, and apply inheritance or polymorphism. - Custom equality, hashing, or string representation is necessary. In summary, use records for simple data representation and classes for more complex entities with behavior, state, and potential inheritance. Choose based on the specific requirements and nature of the data or entity you are modeling. </details> ### Models Create a new package below your root package called `models`. 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. #### Users We being with our Users 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 userId nullable so when creating a new category, we are not required to supply a categoryId which we will want Firebase to generate. ```java= @Data //creates setters and getters automatically @AllArgsConstructor //creates constructor with all values automatically @NoArgsConstructor //creates no argument constructor automatically public class Users { @DocumentId private @Nullable String userId; private String username; private String email; private List<String> roles; private String image; } ``` #### Tasks Now, we will move on to our tasks Collection. You will note that this is broken into three(3) classes: ATasks, Tasks, and RestTasks. 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.** ##### ATasks (Super Class) The ATasks class is an abstract class meaning we will never create an instance of it. It is the **super class** of the Tasls and RestTasks classes. We put all the common elements of these classes in this file (ie, all the variables not of time DocumentReference). ```java= @Data @AllArgsConstructor @NoArgsConstructor public abstract class ATasks { @DocumentId protected @Nullable String taskId; protected String title; protected String description; protected String status; protected Timestamp createdAt; protected @Nullable Timestamp updatedAt; protected @Nullable List<Object> comments; protected Timestamp dueDate; protected List<String> labels; public void setCreatedAt(String createdAt) throws ParseException { this.createdAt = Timestamp.fromProto(Timestamps.parse(createdAt)); } public void setUpdatedAt(String updatedAt) throws ParseException { this.updatedAt = Timestamp.fromProto(Timestamps.parse(updatedAt)); } public void updateUpdateDateTime(Timestamp updatedAt) { this.updatedAt = updatedAt; } public void setDueDate(String dueDate) throws ParseException { this.dueDate = Timestamp.fromProto(Timestamps.parse(dueDate)); } public void updateDueDateTime(Timestamp dueDate) { this.dueDate = dueDate; } } ``` ##### Tasks (Sub Class) ```java= @Data @NoArgsConstructor public class Tasks extends ATasks { private List<Users> assignedUsers; private Users createdBy; private List<ChecklistItem> checklist; public Tasks(String taskId, String title, String description, String status, Timestamp createdAt, Timestamp updatedAt, List<Object> comments, Timestamp dueDate, List<String> labels, List<Users> assignedUsers, Users createdBy, List<ChecklistItem> checklist) { super(taskId, title, description, status, createdAt, updatedAt, comments, dueDate, labels); this.assignedUsers = assignedUsers; this.createdBy = createdBy; this.checklist = checklist; } } ``` ##### RestTasks (Sub Class) ```java= @Data @NoArgsConstructor public class RestTasks extends ATasks { private List<DocumentReference> assignedUsers; private DocumentReference createdBy; private List<DocumentReference> checklist; public RestTasks(String taskId, String title, String description, String status, Timestamp createdAt, Timestamp updatedAt, List<Object> comments, Timestamp dueDate, List<String> labels, List<DocumentReference> assignedUsers, DocumentReference createdBy, List<DocumentReference> checklist) { super(taskId, title, description, status, createdAt, updatedAt, comments, dueDate, labels); this.assignedUsers = assignedUsers; this.createdBy = createdBy; this.checklist = checklist; } // Setters and Getters for String parameters to perform Firestore queries public void setCreatedBy(String createdBy) { // Perform Firebase Firestore query to retrieve DocumentReference for createBy this.createdBy = Utility.retrieveDocumentReference("Users", createdBy); } public void setAssignedUsers(ArrayList<String> assignedUsers) { this.assignedUsers = new ArrayList<>(); for(String user : assignedUsers) { this.assignedUsers.add(Utility.retrieveDocumentReference("Users", user))); } } public void setChecklist(ArrayList<String> checklists) { this.checklist = new ArrayList<>(); for(String listId: checklists) { this.checklist.add(Utility.retrieveDocumentReference("ChecklistItem", listId))); } } } ``` #### Messages Similar to the `Tasks` class, `Messages` will be divied into three classes due to the use of reference type values (foreign keys) ##### AMessages (Super Class) ```java= @Data @NoArgsConstructor @AllArgsConstructor public abstract class AMessages { @DocumentId private @Nullable String messageId; private String content; private Timestamp timestamp; public void setTimestamp(String timestamp) throws ParseException { this.timestamp = Timestamp.fromProto(Timestamps.parse(timestamp)); } } ``` ##### Messages (Sub Class) ```java= @Data @NoArgsConstructor public class Messages extends AMessages { private Users senderId; private Users receiverId; public Messages(@Nullable String messageId, String content, Timestamp timestamp, Users senderId, Users receiverId) { super(messageId, content, timestamp); this.senderId = senderId; this.receiverId = receiverId; } } ``` ##### RestMessages (Sub Class) ```java= @Data @NoArgsConstructor public class RestMessages extends AMessages{ private DocumentReference senderId; private DocumentReference receiverId; public RestMessages(@Nullable String messageId, String content, Timestamp timestamp, DocumentReference senderId, DocumentReference receiverId) { super(messageId, content, timestamp); this.senderId = senderId; this.receiverId = receiverId; } public void setSenderId(String senderId) { this.senderId = Utility.retrieveDocumentReference("Users", senderId); } public void setReceiverId(String receiverId) { this.receiverId = Utility.retrieveDocumentReference("Users", receiverId); } } ``` #### ChecklistItem `ChecklistItem` is similar to our `Users` class. It only contains simple data type, so it does not require additional classes. ```java= @Data @NoArgsConstructor @AllArgsConstructor public class ChecklistItem { @DocumentId private String checklistId; private String item; private boolean isComplete; public boolean getIsComplete() { return isComplete; } public void setIsComplete(boolean complete) { isComplete = complete; } } ``` _**NB:** We add a setter and getter for our boolean variable because the `@Data` annotation will not create a method with the appropriate naming convention expected by Spring Boot._ #### SharedTasks `SharedTasks` contains foreign key values; however, in this case we will create **two** rather than three classes. The only shared value that would be found in the abstract class would the `sharedTaskId`; therefore, it is easier to not make the abstract class. ##### SharedTasks ```java= @Data @NoArgsConstructor @AllArgsConstructor public class SharedTasks { @DocumentId private String sharedTaskId; private Tasks taskId; private ArrayList<Users> sharedWithUsers; } ``` ##### RestSharedTasks ```java= Data @NoArgsConstructor @AllArgsConstructor public class RestSharedTasks { @DocumentId private String sharedTaskId; private DocumentReference taskId; private ArrayList<DocumentReference> sharedWithUsers; public void setTaskId(String taskId) { // Perform Firebase Firestore query to retrieve DocumentReference for createBy this.taskId = Utility.retrieveDocumentReference("Tasks", taskId); } public void setSharedWithUsers(ArrayList<String> sharedWithUsers) { this.sharedWithUsers = new ArrayList<>(); for(String user : sharedWithUsers) { this.sharedWithUsers.add(Utility.retrieveDocumentReference("Users", user)); } } } ```