| 3 | | This page documents the advanced application development implemented for Phase P8 of the Room Reservation System project. The goal of this phase is to extend the Java prototype application with transaction handling and database connection pooling. |
| 4 | | |
| 5 | | The prototype was extended in two main directions: |
| 6 | | |
| 7 | | * database connection pooling was implemented using HikariCP; |
| 8 | | * explicit transaction handling was added for use-cases that execute multiple related database operations. |
| 9 | | |
| 10 | | The implementation affects the following source files: |
| 11 | | |
| 12 | | * ''src/main/java/mk/finki/roomreservation/App.java'' |
| 13 | | * ''src/main/java/mk/finki/roomreservation/DatabasePool.java'' |
| 14 | | * ''pom.xml'' |
| 15 | | |
| 16 | | The application still connects to the same PostgreSQL database schema named ''project'' through the SSH tunnel on ''localhost:9999''. |
| 17 | | |
| 18 | | == Database Connection Pooling == |
| 19 | | |
| 20 | | === Description === |
| 21 | | |
| 22 | | In the previous version of the prototype, the application used a direct JDBC connection. In Phase P8, the application was changed to use a database connection pool. |
| 23 | | |
| 24 | | Connection pooling is useful because database connections are expensive to create repeatedly. Instead of creating a new physical database connection for every operation, the application initializes a pool of reusable connections. When a use-case needs database access, the application requests a connection from the pool, uses it, and then returns it to the pool. |
| 25 | | |
| 26 | | The connection pool is implemented in the class: |
| 27 | | |
| 28 | | {{{ |
| 29 | | src/main/java/mk/finki/roomreservation/DatabasePool.java |
| 30 | | }}} |
| 31 | | |
| 32 | | The Maven configuration file ''pom.xml'' was also updated with the required HikariCP dependency. |
| 33 | | |
| 34 | | === Implementation === |
| 35 | | |
| 36 | | The application initializes the connection pool after the user enters the database URL, database username, and database password. If the pool is created successfully, the application displays a confirmation message and then shows the main menu. |
| 37 | | |
| 38 | | [[Image(p8_connection_pool_initialized.png, width=100%)]] |
| 39 | | |
| 40 | | The screenshot shows that the HikariCP connection pool starts successfully. The application then confirms that the database connection pool was initialized and that the database connection is successful. |
| 41 | | |
| 42 | | The important confirmation messages are: |
| 43 | | |
| 44 | | {{{ |
| 45 | | Database connection pool initialized successfully. |
| 46 | | |
| 47 | | Connected to database successfully. |
| 48 | | }}} |
| 49 | | |
| 50 | | After the connection pool is initialized, the application displays the main menu: |
| 51 | | |
| 52 | | {{{ |
| 53 | | Main menu |
| 54 | | |
| 55 | | 1. Search available rooms |
| 56 | | 2. Create reservation request |
| 57 | | 3. Approve or reject reservation |
| 58 | | 4. Exit |
| 59 | | Choose option: |
| 60 | | }}} |
| 61 | | |
| 62 | | === Usage in the application === |
| 63 | | |
| 64 | | The application uses connections from the pool in the implemented use-cases. A connection is requested from the pool before executing SQL statements. When the operation finishes, the connection is closed in the Java code, but this does not destroy the physical database connection. Instead, the connection is returned to the pool and can be reused. |
| 65 | | |
| 66 | | This improves the structure of the application because database access is managed through a separate pooling class instead of being handled manually in every part of the application. |
| 67 | | |
| 68 | | == Transactions == |
| 69 | | |
| 70 | | Transactions are required when one logical operation contains multiple database operations that must succeed or fail together. |
| 71 | | |
| 72 | | In this prototype, transactions are used in the following implemented use-cases: |
| 73 | | |
| 74 | | * ''Create reservation request'' |
| 75 | | * ''Approve or reject reservation'' |
| 76 | | |
| 77 | | The application uses explicit transaction control in Java. The general transaction structure is: |
| 78 | | |
| 79 | | {{{ |
| 80 | | connection.setAutoCommit(false); |
| 81 | | |
| 82 | | try { |
| 83 | | // execute several related SQL statements |
| 84 | | connection.commit(); |
| 85 | | } catch (SQLException e) { |
| 86 | | connection.rollback(); |
| 87 | | throw e; |
| 88 | | } finally { |
| 89 | | connection.setAutoCommit(true); |
| 90 | | } |
| 91 | | }}} |
| 92 | | |
| 93 | | This means that all database changes inside the transaction are saved only if the complete operation is successful. If an error occurs, the transaction is rolled back and the database is not left in a partially modified state. |
| 94 | | |
| 95 | | == Transaction 1: Create reservation request == |
| 96 | | |
| 97 | | === Data requirements description === |
| 98 | | |
| 99 | | Creating a reservation request may require changes in more than one table. |
| 100 | | |
| 101 | | A reservation request can include: |
| 102 | | |
| 103 | | * only a room; |
| 104 | | * only requested equipment; |
| 105 | | * both a room and requested equipment. |
| 106 | | |
| 107 | | When the requester creates a reservation with additional equipment, the application must insert data into: |
| 108 | | |
| 109 | | * ''project.reservations'' |
| 110 | | * ''project.reservation_equipment'' |
| 111 | | |
| 112 | | These operations must be executed as one transaction. If the reservation is inserted but inserting the requested equipment fails, the whole operation should be rolled back. This prevents incomplete reservation data from being saved. |
| 113 | | |
| 114 | | === Implementation === |
| 115 | | |
| 116 | | The transaction is implemented in the ''Create reservation request'' use-case in ''App.java''. |
| 117 | | |
| 118 | | The application performs the following steps: |
| 119 | | |
| 120 | | 1. The requester chooses the option ''Create reservation request'' from the main menu. |
| 121 | | 2. The system displays a list of users. |
| 122 | | 3. The requester selects the requester from the list. |
| 123 | | 4. The system asks for the reservation date. |
| 124 | | 5. The requester enters the reservation date. |
| 125 | | 6. The system asks for the start time and end time. |
| 126 | | 7. The requester enters the requested time interval. |
| 127 | | 8. The system asks whether the reservation should include a room. |
| 128 | | 9. If the reservation includes a room, the system displays available rooms. |
| 129 | | 10. The requester selects one of the available rooms from the list. |
| 130 | | 11. The system asks whether additional/general equipment should be requested. |
| 131 | | 12. If equipment is requested, the requester selects equipment and requested quantity. |
| 132 | | 13. The application starts a transaction. |
| 133 | | 14. The application inserts a new record into ''project.reservations''. |
| 134 | | 15. If requested equipment exists, the application inserts corresponding records into ''project.reservation_equipment''. |
| 135 | | 16. If all operations are successful, the transaction is committed. |
| 136 | | 17. If an error occurs, the transaction is rolled back. |
| 137 | | |
| 138 | | === Demo execution === |
| 139 | | |
| 140 | | The following screenshot shows the successful execution of the create reservation transaction. |
| 141 | | |
| 142 | | [[Image(p8_create_reservation_transaction_success.png, width=100%)]] |
| 143 | | |
| 144 | | In this test, the requester creates a reservation request through the Java prototype. The application displays available users and rooms, the requester selects values from the displayed lists, and the reservation request is created successfully. |
| 145 | | |
| 146 | | The use-case demonstrates that the user does not need to remember internal database identifiers such as ''user_id'' or ''room_id''. Instead, the application lists the possible values and internally uses the selected database identifiers. |
| 147 | | |
| 148 | | === Database verification === |
| 149 | | |
| 150 | | The result can be verified in DBeaver by checking the created reservation and the requested equipment records. |
| 151 | | |
| 152 | | {{{ |
| 153 | | SELECT * |
| 154 | | FROM project.reservations |
| 155 | | ORDER BY reservation_id DESC; |
| 156 | | |
| 157 | | SELECT * |
| 158 | | FROM project.reservation_equipment |
| 159 | | ORDER BY reservation_id DESC; |
| 160 | | }}} |
| 161 | | |
| 162 | | The following screenshot shows the database verification after the transaction was executed. |
| 163 | | |
| 164 | | [[Image(p8_create_reservation_database_verification.png, width=100%)]] |
| 165 | | |
| 166 | | The screenshot confirms that the reservation was inserted into ''project.reservations''. If the reservation included additional requested equipment, the related records are also inserted into ''project.reservation_equipment''. |
| 167 | | |
| 168 | | === Transaction discussion === |
| 169 | | |
| 170 | | This use-case requires a transaction because creating a reservation request is one logical operation, even when it modifies multiple tables. The reservation and its requested equipment must be saved together. If one part fails, the whole operation should be cancelled. |
| 171 | | |
| 172 | | Therefore, the application commits the transaction only after all related inserts are successful. |
| 173 | | |
| 174 | | == Transaction 2: Approve or reject reservation == |
| 175 | | |
| 176 | | === Data requirements description === |
| 177 | | |
| 178 | | Approving or rejecting a reservation also requires multiple related database operations. |
| 179 | | |
| 180 | | When an approver makes a decision, the application must: |
| 181 | | |
| 182 | | * update the reservation status in ''project.reservations''; |
| 183 | | * insert a new approval decision into ''project.approvals''. |
| 184 | | |
| 185 | | These two operations must be consistent. The reservation status and the approval record must match. If the approval record cannot be inserted, the reservation status should not remain changed. |
| 186 | | |
| 187 | | === Implementation === |
| 188 | | |
| 189 | | The transaction is implemented in the ''Approve or reject reservation'' use-case in ''App.java''. |
| 190 | | |
| 191 | | The application performs the following steps: |
| 192 | | |
| 193 | | 1. The approver chooses the option ''Approve or reject reservation'' from the main menu. |
| 194 | | 2. The system displays a list of pending reservations. |
| 195 | | 3. The approver selects one reservation from the list. |
| 196 | | 4. The system displays a list of users who can approve reservations. |
| 197 | | 5. The approver selects the approving user. |
| 198 | | 6. The approver chooses whether the reservation should be approved or rejected. |
| 199 | | 7. The approver enters an optional decision note. |
| 200 | | 8. The application starts a transaction. |
| 201 | | 9. The application updates the selected reservation status in ''project.reservations''. |
| 202 | | 10. The application inserts a new approval record into ''project.approvals''. |
| 203 | | 11. If both operations are successful, the transaction is committed. |
| 204 | | 12. If an error occurs, the transaction is rolled back. |
| 205 | | |
| 206 | | === Demo execution === |
| 207 | | |
| 208 | | The following screenshot shows the successful execution of the approval transaction. |
| 209 | | |
| 210 | | [[Image(p8_approval_transaction_success.png, width=100%)]] |
| 211 | | |
| 212 | | The screenshot shows that the approver selects a pending reservation, chooses the approver user, enters the decision, and saves the approval decision. The application then displays the final reservation details. |
| 213 | | |
| 214 | | === Database verification === |
| 215 | | |
| 216 | | The result can be verified in DBeaver by checking the reservations and approvals tables. |
| 217 | | |
| 218 | | {{{ |
| 219 | | SELECT * |
| 220 | | FROM project.reservations |
| 221 | | ORDER BY reservation_id DESC; |
| 222 | | |
| 223 | | SELECT * |
| 224 | | FROM project.approvals |
| 225 | | ORDER BY approval_id DESC; |
| 226 | | }}} |
| 227 | | |
| 228 | | The following screenshot shows the database verification after the approval transaction was executed. |
| 229 | | |
| 230 | | [[Image(p8_approval_database_verification.png, width=100%)]] |
| 231 | | |
| 232 | | The screenshot confirms that the reservation status was updated and that a corresponding approval record was inserted into ''project.approvals''. |
| 233 | | |
| 234 | | === Transaction discussion === |
| 235 | | |
| 236 | | This use-case requires a transaction because approval is not only a single table change. The status update and the approval insert represent one logical decision. |
| 237 | | |
| 238 | | If the reservation status were updated without inserting an approval record, the database would lose information about who made the decision, when the decision was made, and what note was entered. Therefore, both operations must be committed together or rolled back together. |
| 239 | | |
| 240 | | == Rollback behavior == |
| 241 | | |
| 242 | | The transaction implementation protects the database from partial changes. |
| 243 | | |
| 244 | | If an error occurs during the create reservation transaction, the inserted reservation and related requested equipment records are rolled back. If an error occurs during the approval transaction, the reservation status update and the approval insert are rolled back. |
| 245 | | |
| 246 | | This behavior preserves database consistency because incomplete multi-step operations are not saved. |
| 247 | | |
| 248 | | == Maven configuration == |
| 249 | | |
| 250 | | The Maven configuration file ''pom.xml'' was updated to include the required dependencies. |
| 251 | | |
| 252 | | The application uses: |
| 253 | | |
| 254 | | * PostgreSQL JDBC driver for connecting to the PostgreSQL database; |
| 255 | | * HikariCP for database connection pooling; |
| 256 | | * SLF4J implementation for HikariCP logging. |
| 257 | | |
| 258 | | The relevant configuration is stored in: |
| 259 | | |
| 260 | | {{{ |
| 261 | | pom.xml |
| 262 | | }}} |
| 263 | | |
| 264 | | == Final discussion == |
| 265 | | |
| 266 | | Phase P8 improves the Java prototype by adding transaction handling and database connection pooling. |
| 267 | | |
| 268 | | The most important improvements are: |
| 269 | | |
| 270 | | * the application uses HikariCP connection pooling; |
| 271 | | * database access is centralized through ''DatabasePool.java''; |
| 272 | | * the connection pool is initialized once and reused by the application; |
| 273 | | * creating a reservation request is executed as a transaction; |
| 274 | | * inserting requested equipment is included in the same transaction as the reservation insert; |
| 275 | | * approving or rejecting a reservation is executed as a transaction; |
| 276 | | * reservation status updates and approval records are saved consistently; |
| 277 | | * rollback is used when a multi-step database operation fails; |
| 278 | | * the prototype is more reliable and closer to a real database application. |
| 279 | | |
| 280 | | These additions satisfy the Phase P8 requirements because the application now demonstrates both database connection pooling and transactions for use-cases where multiple related database operations must be executed as one unit of work. |
| | 3 | == Overview == |
| | 4 | |
| | 5 | This phase documents the advanced application-level implementation of the Room Reservation System. The final application is implemented as a full-stack system with a Spring Boot backend and a React frontend. |
| | 6 | |
| | 7 | The focus of this phase is on: |
| | 8 | |
| | 9 | * database transactions |
| | 10 | * connection pooling |
| | 11 | * database access through the backend application |
| | 12 | * consistency between the documented implementation and the source code uploaded in the project Git repository |
| | 13 | |
| | 14 | The relevant source files are available in the project repository: |
| | 15 | |
| | 16 | * [source:backend/src/main/resources/application.properties backend/src/main/resources/application.properties] |
| | 17 | * [source:backend/src/main/java/mk/finki/roomreservation/service/ReservationService.java backend/src/main/java/mk/finki/roomreservation/service/ReservationService.java] |
| | 18 | * [source:backend/src/main/java/mk/finki/roomreservation/service/AuthService.java backend/src/main/java/mk/finki/roomreservation/service/AuthService.java] |
| | 19 | * [source:backend/src/main/java/mk/finki/roomreservation/service/ApprovalService.java backend/src/main/java/mk/finki/roomreservation/service/ApprovalService.java] |
| | 20 | * [source:backend/src/main/java/mk/finki/roomreservation/controller/ReservationController.java backend/src/main/java/mk/finki/roomreservation/controller/ReservationController.java] |
| | 21 | * [source:backend/src/main/java/mk/finki/roomreservation/controller/AuthController.java backend/src/main/java/mk/finki/roomreservation/controller/AuthController.java] |
| | 22 | * [source:backend/src/main/java/mk/finki/roomreservation/controller/ApprovalController.java backend/src/main/java/mk/finki/roomreservation/controller/ApprovalController.java] |
| | 23 | |
| | 24 | == Connection Pooling == |
| | 25 | |
| | 26 | Connection pooling is implemented through Spring Boot's default HikariCP integration. The backend application uses `JdbcTemplate` for database access, while Spring Boot manages database connections internally through a HikariCP connection pool. |
| | 27 | |
| | 28 | The pool configuration is placed in: |
| | 29 | |
| | 30 | [source:backend/src/main/resources/application.properties backend/src/main/resources/application.properties] |
| | 31 | |
| | 32 | The relevant configuration is: |
| | 33 | |
| | 34 | {{{ |
| | 35 | spring.datasource.hikari.pool-name=RoomReservationPool |
| | 36 | spring.datasource.hikari.maximum-pool-size=5 |
| | 37 | spring.datasource.hikari.minimum-idle=1 |
| | 38 | spring.datasource.hikari.connection-timeout=10000 |
| | 39 | }}} |
| | 40 | |
| | 41 | The database connection parameters are also configured in the same file: |
| | 42 | |
| | 43 | {{{ |
| | 44 | spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:9999/db_202526z_va_prj_room_reservation} |
| | 45 | spring.datasource.username=${SPRING_DATASOURCE_USERNAME:db_202526z_va_prj_room_reservation_owner} |
| | 46 | spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:} |
| | 47 | }}} |
| | 48 | |
| | 49 | The actual database password is not hard-coded in the source code. It is provided through an environment variable. This makes the application safer and allows the same codebase to run locally and through Docker. |
| | 50 | |
| | 51 | The backend services do not manually open and close raw JDBC connections. Instead, they use Spring's `JdbcTemplate`. When a query is executed, Spring obtains a connection from the HikariCP pool, executes the SQL operation, and returns the connection back to the pool. |
| | 52 | |
| | 53 | == Transaction 1: Creating a Reservation Request == |
| | 54 | |
| | 55 | The main application-level transaction is implemented in: |
| | 56 | |
| | 57 | [source:backend/src/main/java/mk/finki/roomreservation/service/ReservationService.java backend/src/main/java/mk/finki/roomreservation/service/ReservationService.java] |
| | 58 | |
| | 59 | The transaction is used in the `createReservation` method. This method inserts a new reservation into `project.reservations` and then optionally inserts one or more related equipment records into `project.reservation_equipment`. |
| | 60 | |
| | 61 | The method is marked with `@Transactional`: |
| | 62 | |
| | 63 | {{{ |
| | 64 | @Transactional |
| | 65 | public ReservationDetails createReservation(CreateReservationRequest request) { |
| | 66 | validateCreateRequest(request); |
| | 67 | |
| | 68 | LocalDate date = LocalDate.parse(request.reservationDate()); |
| | 69 | LocalTime start = LocalTime.parse(request.startTime()); |
| | 70 | LocalTime end = LocalTime.parse(request.endTime()); |
| | 71 | |
| | 72 | List<EquipmentRequest> equipment = |
| | 73 | request.equipment() == null ? List.of() : request.equipment(); |
| | 74 | |
| | 75 | String insertReservationSql = """ |
| | 76 | INSERT INTO project.reservations ( |
| | 77 | room_id, |
| | 78 | user_id, |
| | 79 | reservation_date, |
| | 80 | start_time, |
| | 81 | end_time, |
| | 82 | status |
| | 83 | ) |
| | 84 | VALUES (?, ?, ?, ?, ?, 'pending') |
| | 85 | RETURNING reservation_id |
| | 86 | """; |
| | 87 | |
| | 88 | Integer reservationId = jdbcTemplate.queryForObject( |
| | 89 | insertReservationSql, |
| | 90 | Integer.class, |
| | 91 | request.roomId(), |
| | 92 | request.userId(), |
| | 93 | Date.valueOf(date), |
| | 94 | Time.valueOf(start), |
| | 95 | Time.valueOf(end) |
| | 96 | ); |
| | 97 | |
| | 98 | if (reservationId == null) { |
| | 99 | throw new RuntimeException("Reservation was not created."); |
| | 100 | } |
| | 101 | |
| | 102 | for (EquipmentRequest item : equipment) { |
| | 103 | jdbcTemplate.update( |
| | 104 | """ |
| | 105 | INSERT INTO project.reservation_equipment ( |
| | 106 | reservation_id, |
| | 107 | equipment_id, |
| | 108 | requested_quantity |
| | 109 | ) |
| | 110 | VALUES (?, ?, ?) |
| | 111 | """, |
| | 112 | reservationId, |
| | 113 | item.equipmentId(), |
| | 114 | item.requestedQuantity() |
| | 115 | ); |
| | 116 | } |
| | 117 | |
| | 118 | return findReservationDetails(reservationId); |
| | 119 | } |
| | 120 | }}} |
| | 121 | |
| | 122 | This transaction protects the database from partial writes. |
| | 123 | |
| | 124 | If the reservation insert succeeds, but one of the equipment inserts fails, the entire transaction is rolled back. This means that the system will not store a reservation without its required equipment records. |
| | 125 | |
| | 126 | The method also validates the request before writing to the database: |
| | 127 | |
| | 128 | {{{ |
| | 129 | private void validateCreateRequest(CreateReservationRequest request) { |
| | 130 | if (request.userId() == null) { |
| | 131 | throw new IllegalArgumentException("Requester user is required."); |
| | 132 | } |
| | 133 | |
| | 134 | if (request.reservationDate() == null || request.reservationDate().isBlank()) { |
| | 135 | throw new IllegalArgumentException("Reservation date is required."); |
| | 136 | } |
| | 137 | |
| | 138 | if (request.startTime() == null || request.startTime().isBlank()) { |
| | 139 | throw new IllegalArgumentException("Start time is required."); |
| | 140 | } |
| | 141 | |
| | 142 | if (request.endTime() == null || request.endTime().isBlank()) { |
| | 143 | throw new IllegalArgumentException("End time is required."); |
| | 144 | } |
| | 145 | |
| | 146 | LocalTime start = LocalTime.parse(request.startTime()); |
| | 147 | LocalTime end = LocalTime.parse(request.endTime()); |
| | 148 | |
| | 149 | if (!end.isAfter(start)) { |
| | 150 | throw new IllegalArgumentException("End time must be after start time."); |
| | 151 | } |
| | 152 | |
| | 153 | boolean hasRoom = request.roomId() != null; |
| | 154 | boolean hasEquipment = request.equipment() != null && !request.equipment().isEmpty(); |
| | 155 | |
| | 156 | if (!hasRoom && !hasEquipment) { |
| | 157 | throw new IllegalArgumentException("Reservation must include at least one resource: room or equipment."); |
| | 158 | } |
| | 159 | } |
| | 160 | }}} |
| | 161 | |
| | 162 | This validates that the reservation has a requester, a valid date and time interval, and at least one reserved resource. |
| | 163 | |
| | 164 | == Transaction 2: User Registration == |
| | 165 | |
| | 166 | User registration is implemented in: |
| | 167 | |
| | 168 | [source:backend/src/main/java/mk/finki/roomreservation/service/AuthService.java backend/src/main/java/mk/finki/roomreservation/service/AuthService.java] |
| | 169 | |
| | 170 | The `register` method is also transactional because it writes to two related tables: |
| | 171 | |
| | 172 | * `project.users` |
| | 173 | * `project.user_credentials` |
| | 174 | |
| | 175 | The method first inserts the user profile and then inserts the password hash. |
| | 176 | |
| | 177 | {{{ |
| | 178 | @Transactional |
| | 179 | public UserOption register(RegisterRequest request) { |
| | 180 | validateRegisterRequest(request); |
| | 181 | |
| | 182 | String fullName = request.fullName().trim(); |
| | 183 | String username = request.username().trim(); |
| | 184 | String email = request.email().trim(); |
| | 185 | |
| | 186 | if (usernameExists(username)) { |
| | 187 | throw new IllegalArgumentException("Username is already taken."); |
| | 188 | } |
| | 189 | |
| | 190 | if (emailExists(email)) { |
| | 191 | throw new IllegalArgumentException("Email is already registered."); |
| | 192 | } |
| | 193 | |
| | 194 | Integer userId = jdbcTemplate.queryForObject( |
| | 195 | """ |
| | 196 | INSERT INTO project.users ( |
| | 197 | username, |
| | 198 | email, |
| | 199 | full_name, |
| | 200 | role |
| | 201 | ) |
| | 202 | VALUES (?, ?, ?, 'regular') |
| | 203 | RETURNING user_id |
| | 204 | """, |
| | 205 | Integer.class, |
| | 206 | username, |
| | 207 | email, |
| | 208 | fullName |
| | 209 | ); |
| | 210 | |
| | 211 | if (userId == null) { |
| | 212 | throw new RuntimeException("User registration failed."); |
| | 213 | } |
| | 214 | |
| | 215 | String passwordHash = passwordEncoder.encode(request.password()); |
| | 216 | |
| | 217 | jdbcTemplate.update( |
| | 218 | """ |
| | 219 | INSERT INTO project.user_credentials ( |
| | 220 | user_id, |
| | 221 | password_hash |
| | 222 | ) |
| | 223 | VALUES (?, ?) |
| | 224 | """, |
| | 225 | userId, |
| | 226 | passwordHash |
| | 227 | ); |
| | 228 | |
| | 229 | return findUserById(userId); |
| | 230 | } |
| | 231 | }}} |
| | 232 | |
| | 233 | This transaction ensures that the application does not create an incomplete account. If the password hash insert fails, the inserted user is rolled back as well. |
| | 234 | |
| | 235 | Passwords are not stored as plain text. The application stores only the BCrypt password hash. |
| | 236 | |
| | 237 | == Transaction 3: Activating an Existing Account == |
| | 238 | |
| | 239 | The application also supports assigning a password to an already existing database user. This is implemented in the same service: |
| | 240 | |
| | 241 | [source:backend/src/main/java/mk/finki/roomreservation/service/AuthService.java backend/src/main/java/mk/finki/roomreservation/service/AuthService.java] |
| | 242 | |
| | 243 | {{{ |
| | 244 | @Transactional |
| | 245 | public UserOption activateExistingAccount(ActivateAccountRequest request) { |
| | 246 | validateActivationRequest(request); |
| | 247 | |
| | 248 | UserOption user = findUserByIdentifier(request.identifier()); |
| | 249 | |
| | 250 | if (credentialsExist(user.userId())) { |
| | 251 | throw new IllegalArgumentException("This account already has a password."); |
| | 252 | } |
| | 253 | |
| | 254 | String passwordHash = passwordEncoder.encode(request.password()); |
| | 255 | |
| | 256 | jdbcTemplate.update( |
| | 257 | """ |
| | 258 | INSERT INTO project.user_credentials ( |
| | 259 | user_id, |
| | 260 | password_hash |
| | 261 | ) |
| | 262 | VALUES (?, ?) |
| | 263 | """, |
| | 264 | user.userId(), |
| | 265 | passwordHash |
| | 266 | ); |
| | 267 | |
| | 268 | return user; |
| | 269 | } |
| | 270 | }}} |
| | 271 | |
| | 272 | This keeps the user account and credential creation consistent. |
| | 273 | |
| | 274 | == Stored Function Call for Approval Workflow == |
| | 275 | |
| | 276 | Approving or rejecting a reservation is implemented in: |
| | 277 | |
| | 278 | [source:backend/src/main/java/mk/finki/roomreservation/service/ApprovalService.java backend/src/main/java/mk/finki/roomreservation/service/ApprovalService.java] |
| | 279 | |
| | 280 | The Java backend calls a PostgreSQL stored function: |
| | 281 | |
| | 282 | {{{ |
| | 283 | public ApprovalResult approveOrReject(ApprovalRequest request) { |
| | 284 | validateApprovalRequest(request); |
| | 285 | |
| | 286 | String sql = """ |
| | 287 | SELECT * |
| | 288 | FROM project.fn_approve_or_reject_reservation( |
| | 289 | ?, |
| | 290 | ?, |
| | 291 | CAST(? AS project.approval_decision_domain), |
| | 292 | ? |
| | 293 | ) |
| | 294 | """; |
| | 295 | |
| | 296 | ApprovalResult result = jdbcTemplate.queryForObject( |
| | 297 | sql, |
| | 298 | (rs, rowNum) -> new ApprovalResult( |
| | 299 | rs.getInt("out_approval_id"), |
| | 300 | rs.getInt("out_reservation_id"), |
| | 301 | rs.getString("out_status"), |
| | 302 | rs.getString("out_decision"), |
| | 303 | rs.getString("out_decision_time") |
| | 304 | ), |
| | 305 | request.reservationId(), |
| | 306 | request.approverId(), |
| | 307 | request.decision(), |
| | 308 | request.note() |
| | 309 | ); |
| | 310 | |
| | 311 | if (result == null) { |
| | 312 | throw new RuntimeException("Approval operation failed."); |
| | 313 | } |
| | 314 | |
| | 315 | return result; |
| | 316 | } |
| | 317 | }}} |
| | 318 | |
| | 319 | The approval logic is intentionally placed inside the database stored function. This demonstrates cooperation between the application layer and the database layer. |
| | 320 | |
| | 321 | The backend validates the request before calling the function: |
| | 322 | |
| | 323 | {{{ |
| | 324 | private void validateApprovalRequest(ApprovalRequest request) { |
| | 325 | if (request.reservationId() <= 0) { |
| | 326 | throw new IllegalArgumentException("Reservation ID is required."); |
| | 327 | } |
| | 328 | |
| | 329 | if (request.approverId() <= 0) { |
| | 330 | throw new IllegalArgumentException("Approver ID is required."); |
| | 331 | } |
| | 332 | |
| | 333 | if (request.decision() == null || request.decision().isBlank()) { |
| | 334 | throw new IllegalArgumentException("Decision is required."); |
| | 335 | } |
| | 336 | |
| | 337 | if (!request.decision().equals("approved") && !request.decision().equals("rejected")) { |
| | 338 | throw new IllegalArgumentException("Decision must be approved or rejected."); |
| | 339 | } |
| | 340 | } |
| | 341 | }}} |
| | 342 | |
| | 343 | == Controllers Using the Transactional Services == |
| | 344 | |
| | 345 | The transactional service methods are exposed through REST controllers. |
| | 346 | |
| | 347 | Reservation creation is exposed through: |
| | 348 | |
| | 349 | [source:backend/src/main/java/mk/finki/roomreservation/controller/ReservationController.java backend/src/main/java/mk/finki/roomreservation/controller/ReservationController.java] |
| | 350 | |
| | 351 | {{{ |
| | 352 | @PostMapping("/api/reservations") |
| | 353 | public ReservationDetails createReservation(@RequestBody CreateReservationRequest request) { |
| | 354 | return reservationService.createReservation(request); |
| | 355 | } |
| | 356 | }}} |
| | 357 | |
| | 358 | User registration and login are exposed through: |
| | 359 | |
| | 360 | [source:backend/src/main/java/mk/finki/roomreservation/controller/AuthController.java backend/src/main/java/mk/finki/roomreservation/controller/AuthController.java] |
| | 361 | |
| | 362 | {{{ |
| | 363 | @PostMapping("/register") |
| | 364 | public UserOption register(@RequestBody RegisterRequest request) { |
| | 365 | return authService.register(request); |
| | 366 | } |
| | 367 | }}} |
| | 368 | |
| | 369 | Approval is exposed through: |
| | 370 | |
| | 371 | [source:backend/src/main/java/mk/finki/roomreservation/controller/ApprovalController.java backend/src/main/java/mk/finki/roomreservation/controller/ApprovalController.java] |
| | 372 | |
| | 373 | {{{ |
| | 374 | @PostMapping("/api/approvals") |
| | 375 | public ApprovalResult approveOrReject(@RequestBody ApprovalRequest request) { |
| | 376 | return approvalService.approveOrReject(request); |
| | 377 | } |
| | 378 | }}} |
| | 379 | |
| | 380 | == Error Handling and Rollback Behavior == |
| | 381 | |
| | 382 | The application uses a global exception handler: |
| | 383 | |
| | 384 | [source:backend/src/main/java/mk/finki/roomreservation/exception/ApiExceptionHandler.java backend/src/main/java/mk/finki/roomreservation/exception/ApiExceptionHandler.java] |
| | 385 | |
| | 386 | If a validation error or database error occurs, the backend returns a structured error response to the frontend. |
| | 387 | |
| | 388 | Because the important write operations are marked with `@Transactional`, Spring rolls back the transaction when a runtime exception occurs. |
| | 389 | |
| | 390 | This behavior is important for: |
| | 391 | |
| | 392 | * reservation creation |
| | 393 | * user registration |
| | 394 | * account activation |
| | 395 | |
| | 396 | == Summary == |
| | 397 | |
| | 398 | The final application implements advanced application development concepts through: |
| | 399 | |
| | 400 | * Spring Boot REST controllers |
| | 401 | * service-layer database access |
| | 402 | * Spring `JdbcTemplate` |
| | 403 | * HikariCP connection pooling |
| | 404 | * transaction management with `@Transactional` |
| | 405 | * database stored function integration |
| | 406 | * environment-based database credentials |
| | 407 | * BCrypt password hashing |
| | 408 | |
| | 409 | The documentation is aligned with the final Git source code. The previously used prototype class names are no longer referenced. The current implementation documents the actual Spring Boot files that are present in the repository. |