--- tags: firebase,authentication,react,spring-boot,fall23 --- # Airline (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 = AirlineSpringApplication.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. ### 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. #### Passangers We being with our Passenger 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 passengerID 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 Passenger { @DocumentId private @Nullable String passengerID; private String firstName; private String lastName; private Timestamp dateOfBirth; //use google cloud timestamp private String email; private String phone; private String address; private String rewardsNumber; public void setDateOfBirth(String dateOfBirth) throws ParseException { this.dateOfBirth = Timestamp.fromProto(Timestamps.parse(dateOfBirth)); } } ``` `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. 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 #### Flight ```java= @Data @AllArgsConstructor @NoArgsConstructor public class Flight { @DocumentId private @Nullable String flightNumber; private String departureAirport; private String arrivalAirport; private Timestamp departureDateTime; private Timestamp arrivalDateTime; private String aircraftType; private String flightStatus; private long mileage; private Map<String,Long> availableSeats; public void setDepartureDateTime(String departureDateTime) throws ParseException { this.departureDateTime = Timestamp.fromProto(Timestamps.parse(departureDateTime)); } public void setArrivalDateTime(String arrivalDateTime) throws ParseException { this.arrivalDateTime = Timestamp.fromProto(Timestamps.parse(arrivalDateTime)); } } ``` #### Aircraft ```java= @Data @AllArgsConstructor @NoArgsConstructor public class Aircraft { @DocumentId private @Nullable String aircraftID; private String aircraftType; private long firstClassSeats; private long businessClassSeats; private long mainClassSeats; } ``` #### Airport ```java= @AllArgsConstructor @NoArgsConstructor @Data public class Airport { @DocumentId private @Nullable String airportCode; private String airportName; private String location; } ``` #### User ```java= @AllArgsConstructor @NoArgsConstructor @Data public class User { private @Nullable String userID; // Firebase Authentication UID private String email; private String firstName; private String lastName; private String rewardsNumber; } ``` #### Payment ```java= @Data @AllArgsConstructor @NoArgsConstructor public class Payment { @DocumentId private @Nullable String paymentID; private Timestamp paymentDate; private double amount; private String paymentMethod; private String transactionID; public void setPaymentDate(String paymentDate) throws ParseException { this.paymentDate = Timestamp.fromProto(Timestamps.parse(paymentDate)); } } ``` #### Booking Now, we will move on to our booking Collection. You will note that this is broken into three(3) classes: ABooking, Booking, and RestBooking. 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.** #### ABooking (Super Class) The ABooking 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). ```java= @Data @AllArgsConstructor @NoArgsConstructor public abstract class ABooking { @DocumentId protected @Nullable String bookingID; protected Timestamp bookingDateTime; protected String seatNumber; protected String flightClass; protected double price; public void setBookingDateTime(String bookingDateTime) throws ParseException { this.bookingDateTime = Timestamp.fromProto(Timestamps.parse(bookingDateTime)); } public void updateBookingDateTime(Timestamp bookingDateTime) { this.bookingDateTime = bookingDateTime; } } ``` #### Booking (Sub Class) ```java= @Data @NoArgsConstructor public class Booking extends ABooking { private Passanger passengerID; private Flight flightNumber; private Payment paymentID; public Booking(String bookingID, Timestamp bookingDateTime, String seatNumber, String flightClass, double price, Passanger passengerID, Flight flightNumber, Payment paymentNumber) { super(bookingID, bookingDateTime, seatNumber, flightClass, price); this.passengerID = passengerID; this.flightNumber = flightNumber; this.payment = payment; } } ``` #### RestBooking (Sub Class) ```java= @Data @NoArgsConstructor public class RestBooking extends ABooking{ private DocumentReference passengerID; private DocumentReference flightNumber; private DocumentReference paymentID; // Constructor public RestBooking(String bookingID, Timestamp bookingDateTime, String seatNumber, String flightClass, double price,String passengerID, String flightNumber, String payment) { super(bookingID, bookingDateTime, seatNumber, flightClass, price); setPassengerID(passengerID); setFlightNumber(flightNumber); setPayment(payment); } // Setters and Getters for String parameters to perform Firestore queries public void setPassengerID(String passengerID) { // Perform Firebase Firestore query to retrieve DocumentReference for passengerID this.passengerID = retrieveDocumentReference("Passenger", passengerID); } public void setFlightNumber(String flightNumber) { // Perform Firebase Firestore query to retrieve DocumentReference for flightNumber this.flightNumber = retrieveDocumentReference("Flight", flightNumber); } public void setPayment(String payment) { // Perform Firebase Firestore query to retrieve DocumentReference for paymentNumber this.payment = retrieveDocumentReference("Payment", payment); } // Method to retrieve DocumentReference based on the provided ID private DocumentReference retrieveDocumentReference(String collection, String id) { Firestore db = FirestoreClient.getFirestore(); return db.collection(collection).document(id); } } ``` ### 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. Create a service classe for each database collection. ***Please refer to the [Firebase Documentation](https://firebase.google.com/docs/firestore/quickstart#java_1) for help writing queries.*** #### PassengerService Our PassengerService will include a two method sthat allows us to get all the passangers from the database or passanger based on the ID. 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 Passanger objects. ```java= @Service public class PassengerService { private Firestore firestore; public PassangerService(){ this.firestore = FirestoreClient.getFirestore(); } public Passenger documentSnapshotToPassenger(DocumentSnapshot document) { Passenger passenger = null; if(document.exists()) { passenger = new Passenger(document.getId(),document.getString("firstName"), document.getString("lastName"), document.getTimestamp("dateOfBirth"), document.getString("email"), document.getString("phone"), document.getString("address"), document.getString("rewardsNumber")); } return passenger; } public ArrayList<Passenger> getAllPassengers() throws ExecutionException, InterruptedException { CollectionReference passengerCollection = firestore.collection("Passenger"); ApiFuture<QuerySnapshot> future = passengerCollection.get(); ArrayList<Passenger> passengerList = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { Passenger passenger = documentSnapshotToPassenger(document); if (passenger != null) { passengerList.add(passenger); } } return passengerList; } public Passenger getPassengerById(String passengerId) throws ExecutionException, InterruptedException { CollectionReference passengerCollection = firestore.collection("Passenger"); ApiFuture<DocumentSnapshot> future = passengerCollection.document(passengerId).get(); DocumentSnapshot document = future.get(); return documentSnapshotToPassenger(document); } } ``` 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. #### AirportService ```java= @Service public class AirportService { private Firestore firestore; public AirportService(){ this.firestore = FirestoreClient.getFirestore(); } private Airport documentSnapshotToAirport(DocumentSnapshot document) { if (document.exists()) { return document.toObject(Airport.class); } return null; } public List<Airport> getAllAirports() throws ExecutionException, InterruptedException { CollectionReference airportCollection = firestore.collection("Airport"); ApiFuture<QuerySnapshot> future = airportCollection.get(); List<Airport> airportList = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { Airport airport = documentSnapshotToAirport(document); if (airport != null) { airportList.add(airport); } } return airportList; } public Airport getAirportById(String airportCode) throws ExecutionException, InterruptedException { CollectionReference airportCollection = firestore.collection("Airport"); ApiFuture<DocumentSnapshot> future = airportCollection.document(airportCode).get(); DocumentSnapshot document = future.get(); return documentSnapshotToAirport(document); } } ``` #### FlightService ```java= @Service public class FlightService { private Firestore firestore; public FlightService(){ this.firestore = FirestoreClient.getFirestore(); } public Flight documentSnapshotToFlight(DocumentSnapshot document) { Flight flight = null; if (document.exists()) { flight = new Flight(document.getId(), document.getString("departureAirport"), document.getString("arrivalAirport"), document.getTimestamp("departureDateTime"), document.getTimestamp("arrivalDateTime"), document.getString("aircraftType"), document.getString("flightStatus"), document.getLong("mileage"), (Map<String, Long>) document.get("availableSeats")); } return flight; } public ArrayList<Flight> getAllFlights() throws ExecutionException, InterruptedException { CollectionReference flightCollection = firestore.collection("Flight"); ApiFuture<QuerySnapshot> future = flightCollection.get(); ArrayList<Flight> flightList = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { Flight flight = documentSnapshotToFlight(document); if (flight != null) { flightList.add(flight); } } return flightList; } public Flight getFlightById(String flightId) throws ExecutionException, InterruptedException { CollectionReference flightCollection = firestore.collection("Flight"); ApiFuture<DocumentSnapshot> future = flightCollection.document(flightId).get(); DocumentSnapshot document = future.get(); return documentSnapshotToFlight(document); } } ``` #### AircraftService ```java= @Service public class AircraftService { private Firestore firestore; public AircraftService(){ this.firestore = FirestoreClient.getFirestore(); } private Aircraft documentSnapshotToAircraft(DocumentSnapshot document) { if (document.exists()) { return document.toObject(Aircraft.class); } return null; } public ArrayList<Aircraft> getAllAircraft() throws ExecutionException, InterruptedException { CollectionReference aircraftCollection = firestore.collection("Aircraft"); ApiFuture<QuerySnapshot> future = aircraftCollection.get(); ArrayList<Aircraft> aircraftList = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { Aircraft aircraft = documentSnapshotToAircraft(document); if (aircraft != null) { aircraftList.add(aircraft); } } return aircraftList; } public Aircraft getAircraftById(String aircraftId) throws ExecutionException, InterruptedException { CollectionReference aircraftCollection = firestore.collection("Aircraft"); ApiFuture<DocumentSnapshot> future = aircraftCollection.document(aircraftId).get(); DocumentSnapshot document = future.get(); return documentSnapshotToAircraft(document); } } ``` #### UserService ```java= @Service public class UserService { private Firestore firestore; public UserService(){ this.firestore = FirestoreClient.getFirestore(); } private User documentSnapshotToUser(DocumentSnapshot document) { if (document.exists()) { return document.toObject(User.class); } return null; } public List<User> getAllUsers() throws ExecutionException, InterruptedException { CollectionReference userCollection = firestore.collection("User"); ApiFuture<QuerySnapshot> future = userCollection.get(); List<User> userList = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { User user = documentSnapshotToUser(document); if (user != null) { userList.add(user); } } return userList; } public User getUserById(String userId) throws ExecutionException, InterruptedException { CollectionReference userCollection = firestore.collection("User"); ApiFuture<DocumentSnapshot> future = userCollection.document(userId).get(); DocumentSnapshot document = future.get(); return documentSnapshotToUser(document); } } ``` #### PaymentService ```java= @Service public class PaymentService { private Firestore firestore; public PaymentService(){ this.firestore = FirestoreClient.getFirestore(); } public Payment documentSnapshotToPayment(DocumentSnapshot document) { Payment payment = null; if(document.exists()) { payment = new Payment(document.getId(), document.getTimestamp("paymentDate"), document.getDouble("amount"), document.getString("paymentMethod"), document.getString("transactionID")); } return payment; } public List<Payment> getAllPayments() throws ExecutionException, InterruptedException { CollectionReference paymentCollection = firestore.collection("Payment"); ApiFuture<QuerySnapshot> future = paymentCollection.get(); List<Payment> payments = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { Payment payment = documentSnapshotToPayment(document); if (payment != null) { payments.add(payment); } } return payments; } public Payment getPaymentById(String paymentId) throws ExecutionException, InterruptedException { CollectionReference paymentCollection = firestore.collection("Payment"); ApiFuture<DocumentSnapshot> future = paymentCollection.document(paymentId).get(); DocumentSnapshot document = future.get(); return documentSnapshotToPayment(document); } } ``` #### BookingService The BookingService class includes 4 methods: ##### 1. `documentSnapshotToBooking` This private method takes a Firestore `DocumentSnapshot` and converts it into a `Booking` object. It checks if the document exists and constructs a `Booking` object with the necessary fields. Additionally, it retrieves and sets passenger, flight, and aircraft details associated with the booking by fetching the referenced documents. ##### 2. `getBookingsByFlightNumberAndSort` This method retrieves bookings for a specific flight number from the Firestore database. It uses a query to fetch bookings where the flight number matches the provided flight number, ordering them first by class (first, business, main) and then by seat number. It calls `documentSnapshotToBooking` to convert each document into a `Booking` object, ensuring the associated passenger, flight, and aircraft details are fetched. ##### 3. `getAllBookings` This method retrieves all bookings from the Firestore database. It fetches all documents from the "bookings" collection, converts each document into a `Booking` object using `documentSnapshotToBooking`, and returns a list of all bookings. ##### 4. `getBookingById` This method retrieves a specific booking by its ID from the Firestore database. It uses the provided booking ID to fetch the corresponding document, then calls `documentSnapshotToBooking` to convert the document into a `Booking` object. ```java= @Service public class BookingService { private Firestore firestore; public BookingService(){ this.firestore = FirestoreClient.getFirestore(); } private Booking documentSnapshotToBooking(DocumentSnapshot document) throws ExecutionException, InterruptedException { if (document.exists()) { Booking booking = new Booking(); booking.setBookingID(document.getId()); booking.updateBookingDateTime(document.getTimestamp("bookingDateTime")); booking.setSeatNumber(document.getString("seatNumber")); booking.setPrice(document.getDouble("price")); // Retrieve passenger details DocumentReference passengerRef = (DocumentReference) document.get("passengerID"); if (passengerRef != null) { DocumentSnapshot passengerSnapshot = passengerRef.get().get(); if (passengerSnapshot.exists()) { PassengerService service = new PassengerService(); Passenger passenger = service.documentSnapshotToPassenger(passengerSnapshot); booking.setPassengerID(passenger); } } // Retrieve flight details DocumentReference flightRef = (DocumentReference) document.get("flightNumber"); if (flightRef != null) { DocumentSnapshot flightSnapshot = flightRef.get().get(); if (flightSnapshot.exists()) { FlightService service = new FlightService(); Flight flight = service.documentSnapshotToFlight(flightSnapshot); booking.setFlightNumber(flight); } } // Retrieve payment details DocumentReference paymentRef = (DocumentReference) document.get("paymentID"); if (paymentRef != null) { DocumentSnapshot paymentSnapshot = paymentRef.get().get(); if (paymentSnapshot.exists()) { PaymentService service = new PaymentService(); Payment payment = service.documentSnapshotToPayment(paymentSnapshot); booking.setPaymentID(payment); } } return booking; } return null; } public List<Booking> getAllBookings() throws ExecutionException, InterruptedException { CollectionReference bookingCollection = firestore.collection("Booking"); ApiFuture<QuerySnapshot> future = bookingCollection.get(); List<Booking> bookings = new ArrayList<>(); for (DocumentSnapshot document : future.get().getDocuments()) { Booking booking = documentSnapshotToBooking(document); if (booking != null) { bookings.add(booking); } } return bookings; } public Booking getBookingById(String bookingId) throws ExecutionException, InterruptedException { CollectionReference bookingCollection = firestore.collection("Booking"); ApiFuture<DocumentSnapshot> future = bookingCollection.document(bookingId).get(); DocumentSnapshot document = future.get(); return documentSnapshotToBooking(document); } public List<Booking> getBookingsByFlightNumberAndSort(String flightNumber) throws ExecutionException, InterruptedException { ArrayList<Booking> bookings = null; DocumentReference flightRef = firestore.collection("Flight").document(flightNumber); CollectionReference bookingCollection = firestore.collection("Booking"); Query query = bookingCollection.whereEqualTo("flightNumber", flightRef) .orderBy("flightClass").orderBy("seatNumber", Query.Direction.DESCENDING); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> documents = future.get().getDocuments(); if(!documents.isEmpty()) bookings = new ArrayList<>(); for(QueryDocumentSnapshot document : documents ) { Booking booking = documentSnapshotToBooking(document); if(booking != null) bookings.add(booking); } return bookings; } } ``` ##### `documentSnapshotToBooking` Method Explanation The `documentSnapshotToBooking` method is a private utility method within the `BookingService` class. Its purpose is to transform a Firestore `DocumentSnapshot` into a `Booking` object, encapsulating the necessary booking details. ###### Method Signature ```java private Booking documentSnapshotToBooking(DocumentSnapshot document) throws ExecutionException, InterruptedException ``` ##### Parameters - **`document`**: This parameter is a Firestore `DocumentSnapshot` representing a single document from the "bookings" collection in Firestore. ##### Return Type - **`Booking`**: The method returns a `Booking` object constructed from the data in the provided `DocumentSnapshot`. ##### Method Functionality 1. **Check Document Existence**: The method begins by checking if the `DocumentSnapshot` exists. If the document does not exist, it returns `null`, indicating that no `Booking` can be created. 2. **Construct Booking Object**: If the document exists, the method proceeds to create a `Booking` object using the data from the `DocumentSnapshot`. It invokes `document.toObject(Booking.class)` to convert the document's data into a `Booking` object, which aligns with the `Booking` class structure. 3. **Fetch Referenced Documents**: The method fetches referenced documents associated with the booking, such as the passenger, flight, and aircraft details. It retrieves the relevant `DocumentReference` objects from the `Booking` object. 4. **Retrieve Associated Data**: For each referenced `DocumentReference`, the method fetches the corresponding document from Firestore and converts it to the respective object type (e.g., `Passenger`, `Flight`, `Aircraft`). It sets these associated objects within the `Booking` object. ##### Why Is It Doing This? The `documentSnapshotToBooking` method is designed to centralize the logic for transforming a Firestore document (representing a booking) into a comprehensive `Booking` object. This encapsulation ensures that the complexities of interacting with Firestore and retrieving associated data (passenger, flight, and aircraft) are abstracted away from the calling code. By structuring this logic within a dedicated method, the code becomes modular, maintainable, and easier to understand. The method helps to: - **Abstract Data Transformation**: The method abstracts the transformation of Firestore data (a `DocumentSnapshot`) into a meaningful Java object (`Booking`), promoting clean code and separation of concerns. - **Hide Firestore Interaction Details**: The method hides the intricacies of fetching associated data (passenger, flight, and aircraft) from Firestore, providing a cleaner interface to the calling code. - **Facilitate Reusability**: The method can be reused within the class and across the application, promoting code reusability and reducing redundancy. In summary, `documentSnapshotToBooking` is a crucial utility method that facilitates the transformation of Firestore data into usable `Booking` objects, promoting efficient data handling and abstraction of Firestore interactions. ### Utility Class 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`. Now create a new Java class. 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> ### Controllers Final step is to create our controllers. This is where we will put our endpoint (resource) definitions. Create a packagage called `controller`. Create the following classes in that package `AirportController` , `AircraftController`,`BookingControlelr`, `FlightController`, `PassengerController`, `UserController`, and `PaymentController`. #### PassengerController The PassengerController will make our PassengerService `getAllPassengers` and `getPassengerById` methods 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. - `@GetMapping` annotation for mapping HTTP GET requests onto specific handler methods. - `@PathVariable` an be used to handle template variables in the request URI mapping, and set them as method parameters. ```java= @RestController //identified this class a controller used for REST API class. @RequestMapping("/passenger")//base of the url for all mappings in this controller public class PassengerController { @Autowired private PassengerService passengerService; @GetMapping public ResponseEntity<ApiResponse> getAllPassengers() { try { return ResponseEntity.ok(new ApiResponse(true, "Success", passengerService.getAllPassengers())); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", e.getMessage())); } } @GetMapping("/{passengerId}") public ResponseEntity<ApiResponse> getPassengerById(@PathVariable String passengerId) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", passengerService.getPassengerById(passengerId))); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", e.getMessage())); } } } ``` `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. #### FlightController ```java= @RestController @RequestMapping("/flight") public class FlightController { @Autowired private FlightService flightService; @GetMapping public ResponseEntity<ApiResponse> getAllFlights() { try { return ResponseEntity.ok(new ApiResponse(true, "Success", flightService.getAllFlights())); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", e.getMessage())); } } @GetMapping("/{flightId}") public ResponseEntity<ApiResponse> getFlightById(@PathVariable String flightId) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", flightService.getFlightById(flightId))); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", e.getMessage())); } } } ``` #### AircraftController ```java= @RestController @RequestMapping("/aircraft") public class AircraftController { @Autowired private AircraftService aircraftService; @GetMapping public ResponseEntity<ApiResponse> getAllAircraft() { try { return ResponseEntity.ok(new ApiResponse(true, "Success", aircraftService.getAllAircraft())); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", e.getMessage())); } } @GetMapping("/{aircraftId}") public ResponseEntity<ApiResponse> getAircraftById(@PathVariable String aircraftId) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", aircraftService.getAircraftById(aircraftId))); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", e.getMessage())); } } } ``` #### UserController ```java= @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping public ResponseEntity<ApiResponse> getAllUsers() { try { return ResponseEntity.ok(new ApiResponse(true, "Success", userService.getAllUsers(), null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null, e.getMessage())); } } @GetMapping("/{userId}") public ResponseEntity<ApiResponse> getUserById(@PathVariable String userId) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", userService.getUserById(userId), null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null, e.getMessage())); } } } ``` #### AirportController ```java= @RestController @RequestMapping("/airport") public class AirportController { @Autowired private AirportService airportService; @GetMapping public ResponseEntity<ApiResponse> getAllAirports() { try { return ResponseEntity.ok(new ApiResponse(true, "Success", airportService.getAllAirports(), null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null, e.getMessage())); } } @GetMapping("/{airportCode}") public ResponseEntity<ApiResponse> getAirportById(@PathVariable String airportCode) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", airportService.getAirportById(airportCode), null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null, e.getMessage())); } } } ``` #### PaymentController ```java= @RestController @RequestMapping("/payment") public class PaymentController { @Autowired private PaymentService paymentService; @GetMapping public ResponseEntity<ApiResponse> getAllPayments() { try { List<Payment> payments = paymentService.getAllPayments(); return ResponseEntity.ok(new ApiResponse(true, "Success", payments, null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null, e.getMessage())); } } @GetMapping("/{paymentId}") public ResponseEntity<ApiResponse> getPaymentById(@PathVariable String paymentId) { try { Payment payment = paymentService.getPaymentById(paymentId); if (payment != null) { return ResponseEntity.ok(new ApiResponse(true, "Success", payment, null)); } else { return ResponseEntity.status(404).body(new ApiResponse(false, "Payment not found", null, null)); } } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null, e.getMessage())); } } } ``` #### BookingController ```java= @RestController @RequestMapping("/booking") public class BookingController { @Autowired private BookingService bookingService; @GetMapping("/flight/{flightNumber}") public ResponseEntity<ApiResponse> getBookingsByFlightNumberAndSort(@PathVariable String flightNumber) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", bookingService.getBookingsByFlightNumberAndSort(flightNumber),null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false,"An error occurred", null,e.getMessage())); } } @GetMapping public ResponseEntity<ApiResponse> getAllBookings() { try { return ResponseEntity.ok(new ApiResponse(true, "Success", bookingService.getAllBookings(),null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false,"An error occurred", null,e.getMessage())); } } @GetMapping("/{bookingId}") public ResponseEntity<ApiResponse> getBookingById(@PathVariable String bookingId) { try { return ResponseEntity.ok(new ApiResponse(true, "Success", bookingService.getBookingById(bookingId), null)); } catch (Exception e) { return ResponseEntity.status(500).body(new ApiResponse(false, "An error occurred", null,e.getMessage())); } } } ```