wiki:UseCaseImplementation

Version 20 (modified by 222077, 2 days ago) ( diff )

--

Implementation of user scenarios in the prototype

In the technical prototype of the application implemented so far, the following scenarios have been implemented:

ID UseCase
1Search Routes
2Organize Trip
3Manage Trip
4View My Trips
5Login
6Register

ID 1 - Search Routes

The /search-routes endpoint provides users with the ability to search for available transport routes from different organizers. It supports both viewing all routes by default and filtering routes based on user input (departure and arrival locations). The request is handled by UserRouteController, which interacts with RouteService to retrieve the relevant data.

Here is an example of a search query: -/search-routes?from=sk&to=ohrid

    @RequestMapping("/search-routes")
    public class UserRouteController {
        
        private final RouteService routeService;

        public UserRouteController(RouteService routeService) {
            this.routeService = routeService;
        }

        @GetMapping
        public String findRoutesByFromAndTo(@RequestParam(required = false) String from,
                                            @RequestParam(required = false) String to,
                                            Model model) {
            
            List<Route> filteredRoutes = ((from == null || from.isBlank()) && (to == null || to.isBlank()))
                    ? routeService.findAll()
                    : routeService.findRouteByFromAndToDest(from, to);

            model.addAttribute(filteredRoutes.isEmpty() ? "noRoutesMessage" : "routes",
                    filteredRoutes.isEmpty() ? "No routes found for your search." : filteredRoutes);
            model.addAttribute("display", "user/search-routes");

            return "master";
        }
    }

Controller Details:

  • URL Mapping: /search-routes
  • Method: GET
  • Functionality:
    • Displays all routes when the page is accessed without search criteria.
    • Filters results based on the user's input (from and to).
  • View: Uses the master template and dynamically embeds user/search-routes.

Breakdown:

  • When users first visit /search-routes, the controller calls routeService.findAll() to display all available routes.
  • When users provide search criteria (from and/or to), it filters results using routeService.findRouteByFromAndToDest(from, to).
  • If no matching routes are found, a "No routes found for your search." message is displayed instead of an empty table.
  • The retrieved data is added to the Model, ensuring the front-end can render the appropriate content.

Result of /search-routes

Result of /search-routes?from=sk&to=ohrid

ID 2 - Organize Trip

The /routes endpoint provides transport organizers with the ability to manage their routes and organize trips. It supports viewing authorized routes, viewing trips for a specific route, and adding new trips. The request is handled by CompanyRouteController and CompanyTripController, which interact with CompanyRouteService and CompanyTripService to manage routes and trips.

Here is an example of the flow to organize a trip:

  • View routes: /routes/company
  • View trips for a specific route: /routes/company/view-trips/{routeId}
  • Add a new trip: /routes/company/view-trips/{routeId}/add-trip

    @RequestMapping("/routes")
    public class CompanyRouteController {
        private final CompanyRouteService companyRouteService;

        public CompanyRouteController(CompanyRouteService companyRouteService) {
            this.companyRouteService = companyRouteService;
        }

        @GetMapping("/company")
        public String routes(Model model) {
            model.addAttribute("companyRoutes", companyRouteService.getAuthorizedRoutes());
            model.addAttribute("display", "/company/company-route");

            return "master";
        }
    }

    @RequestMapping("/routes/company/view-trips/{routeId}")
    public class CompanyTripController {
        private final CompanyTripService companyTripService;
        private final RouteService routeService;
        private final LocationService locationService;

        public CompanyTripController(CompanyTripService companyTripService, RouteService routeService, LocationService locationService) {
            this.companyTripService = companyTripService;
            this.routeService = routeService;
            this.locationService = locationService;
        }

        @GetMapping
        public String routeTrips(@PathVariable Integer routeId, Model model) {
            Route route = routeService.findById(routeId);

            model.addAttribute("trips", companyTripService.getAuthorizedTripsByRoute(routeId));
            model.addAttribute("routeId", routeId);
            model.addAttribute("locations", locationService.findAll());
            model.addAttribute("routeSource", route.getSource());
            model.addAttribute("routeDestination", route.getDestination());
            model.addAttribute("display", "/company/company-view-trip");

            return "master";
        }

        @PostMapping("/add-trip")
        public String addNewTrip(@PathVariable Integer routeId,
                                 @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
                                 @RequestParam("freeSeats") int freeSeats,
                                 @RequestParam("locations") List<Integer> locationIds,
                                 @RequestParam("etas") @DateTimeFormat(pattern = "HH:mm") List<LocalTime> etas,
                                 RedirectAttributes redirectAttributes) {

            try {
                Route route = routeService.findById(routeId);
                companyTripService.createTrip(route, date, freeSeats, locationIds, etas);
                redirectAttributes.addFlashAttribute("message", "Trip created successfully!");
            } catch (IllegalArgumentException | SecurityException e) {
                redirectAttributes.addFlashAttribute("error", e.getMessage());
            }
            return "redirect:/routes/company/view-trips/" + routeId;
        }
    }

Controller Details:

  • URL Mapping: /routes
  • Method: GET, POST
  • Functionality:
    • GET /routes/company: Displays authorized routes for the transport organizer.
    • GET /routes/company/view-trips/{routeId}: Displays all authorized trips for the selected route.
    • POST /routes/company/view-trips/{routeId}/add-trip: Allows the transport organizer to add a new trip to an existing route.
  • View: Uses the master template and dynamically embeds /company/company-route and /company/company-view-trip.

Breakdown:

  • When the transport organizer accesses /routes/company, the controller retrieves all authorized routes using companyRouteService.getAuthorizedRoutes() and displays them.
  • When the transport organizer accesses /routes/company/view-trips/{routeId}, the controller retrieves all authorized trips for the specified route using companyTripService.getAuthorizedTripsByRoute(routeId) and displays them along with the locations.
        /**
         * Returns every trip for specific route if user is authorized.
         *
         * @param routeId the routeId for getting authorized trips
         * @return all trips for the route || SecurityException("Unauthorized to access these trips.")
         */
        public List<Trip> getAuthorizedTripsByRoute(Integer routeId) {
            Integer transportOrganizerId = authorizationService.getAuthenticatedTransportOrganizerId();
    
            List<Trip> trips = tripService.findAllByPredicate(TripSpecification.tripsByRoute(routeId));
    
            if (!trips.isEmpty() && !trips.get(0).getTranOrg().getTranOrgId().equals(transportOrganizerId)) {
                throw new SecurityException("Unauthorized to access these trips.");
            }
            return trips;
        }
    
    
  • The transport organizer can add a new trip by submitting the form with details like date, free seats, locations, and estimated times of arrival (ETAs).

Result of /routes/company

Result of /routes/company/view-trips/{routeId}

Result of /routes/company/view-trips/{routeId}/add-trip

ID 3 - Manage Trip

The /routes endpoint allows transport organizers to manage their trips. It includes the ability to view, add, edit, and delete trips for their authorized routes.

Here is an example of the flow to manage trips:

  • View routes: /routes/company
  • View trips for a specific route: /routes/company/view-trips/{routeId}
  • Add a new trip: /routes/company/view-trips/{routeId}/add-trip
  • Edit an existing trip: /routes/company/view-trips/{routeId}/edit-trip/{tripId}
  • Delete a trip: /routes/company/view-trips/{routeId}/delete-trip/{tripId}

    @RequestMapping("/routes")
    public class CompanyRouteController {
        private final CompanyRouteService companyRouteService;

        public CompanyRouteController(CompanyRouteService companyRouteService) {
            this.companyRouteService = companyRouteService;
        }

        @GetMapping("/company")
        public String routes(Model model) {
            model.addAttribute("companyRoutes", companyRouteService.getAuthorizedRoutes());
            model.addAttribute("display", "/company/company-route");

            return "master";
        }
    }

    @RequestMapping("/routes/company/view-trips/{routeId}")
    public class CompanyTripController {
        private final CompanyTripService companyTripService;
        private final RouteService routeService;
        private final LocationService locationService;

        public CompanyTripController(CompanyTripService companyTripService, RouteService routeService, LocationService locationService) {
            this.companyTripService = companyTripService;
            this.routeService = routeService;
            this.locationService = locationService;
        }

        @GetMapping
        public String routeTrips(@PathVariable Integer routeId, Model model) {
            Route route = routeService.findById(routeId);

            model.addAttribute("trips", companyTripService.getAuthorizedTripsByRoute(routeId));
            model.addAttribute("routeId", routeId);
            model.addAttribute("locations", locationService.findAll());
            model.addAttribute("routeSource", route.getSource());
            model.addAttribute("routeDestination", route.getDestination());
            model.addAttribute("display", "/company/company-view-trip");

            return "master";
        }

        @PostMapping("/add-trip")
        public String addNewTrip(@PathVariable Integer routeId,
                                 @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
                                 @RequestParam("freeSeats") int freeSeats,
                                 @RequestParam("locations") List<Integer> locationIds,
                                 @RequestParam("etas") @DateTimeFormat(pattern = "HH:mm") List<LocalTime> etas,
                                 RedirectAttributes redirectAttributes) {

            try {
                Route route = routeService.findById(routeId);
                companyTripService.createTrip(route, date, freeSeats, locationIds, etas);
                redirectAttributes.addFlashAttribute("message", "Trip created successfully!");
            } catch (IllegalArgumentException | SecurityException e) {
                redirectAttributes.addFlashAttribute("error", e.getMessage());
            }
            return "redirect:/routes/company/view-trips/" + routeId;
        }

        @PostMapping("/edit-trip/{tripId}")
        public String editTrip(@PathVariable Integer routeId,
                               @PathVariable Integer tripId,
                               @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
                               @RequestParam("freeSeats") int freeSeats,
                               @RequestParam("locations") List<Integer> locationIds,
                               @RequestParam("etas") @DateTimeFormat(pattern = "HH:mm") List<LocalTime> etas,
                               RedirectAttributes redirectAttributes) {
            try {
                Route route = routeService.findById(routeId);
                companyTripService.updateTrip(route, tripId, date, freeSeats, locationIds, etas);
                redirectAttributes.addFlashAttribute("message", "Trip updated successfully!");
            } catch (IllegalArgumentException | SecurityException e) {
                redirectAttributes.addFlashAttribute("error", e.getMessage());
            }
            return "redirect:/routes/company/view-trips/" + routeId;
        }

        @PostMapping("/delete-trip/{tripId}")
        public String deleteTrip(@PathVariable Integer routeId, @PathVariable Integer tripId) {
            return companyTripService.deleteTripIfAuthorized(tripId)
                    ? "redirect:/routes/company/view-trips/" + routeId
                    : "redirect:/";
        }
    }

Controller Details for Routes:

  • URL Mapping: /routes/company
  • Method: GET
  • Functionality: Displays all authorized routes for the transport organizer.
  • View: Uses the master template and dynamically embeds /company/company-route.

Breakdown:

  • Displays a list of all routes that the transport organizer is authorized to manage.
  • Retrieves authorized routes from the CompanyRouteService and populates them into the Model.
  • The view is displayed using /company/company-route.

Controller Details for Trips:

  • URL Mapping: /routes/company/view-trips/{routeId}
  • Method: GET, POST
  • Functionality:
    • GET /routes/company/view-trips/{routeId}: Displays all trips for the selected route.
    • POST /routes/company/view-trips/{routeId}/add-trip: Allows the transport organizer to add a new trip.
    • POST /routes/company/view-trips/{routeId}/edit-trip/{tripId}: Allows the transport organizer to edit an existing trip.
    • POST /routes/company/view-trips/{routeId}/delete-trip/{tripId}: Allows the transport organizer to delete a trip.
  • View: Uses the master template and dynamically embeds /company/company-view-trip.

Breakdown:

  • Viewing trips: When the transport organizer accesses /routes/company/view-trips/{routeId}, the controller retrieves all trips for the specified route using companyTripService.getAuthorizedTripsByRoute(routeId).
  • Adding a new trip: A form allows the transport organizer to specify the date, free seats, locations, and ETAs for the trip. The controller calls companyTripService.createTrip() to create a new trip.
  • Editing a trip: The transport organizer can modify trip details. The controller calls companyTripService.updateTrip() to update the trip.
  • Deleting a trip: The transport organizer can delete a trip. The controller calls companyTripService.deleteTripIfAuthorized() to remove the trip if the organizer is authorized.
  • Viewing trip stops trip The transport organizer can view and modify trip stops. The controller calls companyTripService.updateTrip() to update whatever the authorized transport organizer wants.

Edit Trip:

EXAMPLE: Free seats modified, changed time and deleted stop.

Delete Trip:

Trip Stops:

ID 4 - View My Trips

The /trips/user endpoint allows authenticated users to view the list of trips they have booked. It retrieves the trips associated with the currently logged-in user and displays them dynamically. The request is handled by UserTripController, which interacts with TripService and AuthenticationService to fetch user-specific trip data.

Example URL for viewing booked trips:

  • /trips/user

    @RequestMapping("/trips")  
    public class UserTripController {  
        private final AuthenticationService authenticationService;  
        private final TripService tripService;  

        public UserTripController(AuthenticationService authenticationService, TripService tripService) {  
            this.authenticationService = authenticationService;  
            this.tripService = tripService;  
        }  

        @GetMapping("/user")  
        public String myTripsPage(Model model) {  
            Integer currentAccountId = authenticationService.getAuthenticatedUserId();  

            model.addAttribute("trips", tripService.findTripsBookedByAccount(currentAccountId));  
            model.addAttribute("display", "user/my-trips");  

            return "master";  
        }  
    }  

Controller Details:

  • URL Mapping: /trips/user
  • Method: GET
  • Functionality:
    • Retrieves the currently logged-in user’s booked trips using tripService.findTripsBookedByAccount(currentAccountId).
    • Passes the retrieved trips to the model under the key "trips".
    • Uses the "master" template and embeds "user/my-trips" dynamically for rendering.

Breakdown:

  • When a user accesses /trips/user, the controller first retrieves the authenticated user's ID using authenticationService.getAuthenticatedUserId().
  • It then fetches all trips booked by the user from tripService.findTripsBookedByAccount(currentAccountId).
  • The retrieved trips are added to the model, ensuring they can be displayed on the front end.

Result of /trips/user

ID-5 - Login

The /login endpoint provides users with authentication functionality through a custom authentication provider. It supports user authentication using email and password credentials, with proper security measures including password encryption and role-based authorization.

Authentication Flow

The login process follows these steps:

  1. User accesses the login page: /login
  2. User submits credentials
  3. System validates and authenticates the user
  4. User is redirected based on authentication result

    @Controller
    @RequestMapping("/login")
    public class LoginController {
        @GetMapping
        public String loginPage(Model model) {
            model.addAttribute("display", "login");
            return "master";
        }
    }

Controller Details:

  • URL Mapping: /login
  • Method: GET
  • Functionality:
    • Displays the login form when accessed
    • Handles authentication through Spring Security configuration
  • View: Uses the master template and dynamically embeds login view

Authentication Provider Implementation:

The CustomAuthenticationProvider handles the core authentication logic:

    @Component
    public class CustomAuthenticationProvider implements AuthenticationProvider {
        private final AccountService accountService;
        private final PasswordEncoder passwordEncoder;

        @Override
        public Authentication authenticate(Authentication authentication) 
                throws AuthenticationException {
            String username = authentication.getName();
            String password = authentication.getCredentials().toString();

            if (username.isEmpty() || password.isEmpty()) {
                throw new BadCredentialsException("Please fill out all fields.");
            }

            try {
                UserDetails userDetails = accountService.findOneByPredicate(
                    AccountSpecification.hasEmail(username));

                if (!passwordEncoder.matches(password, userDetails.getPassword())) {
                    throw new BadCredentialsException("Invalid username or password");
                }
                return new UsernamePasswordAuthenticationToken(
                    userDetails, 
                    userDetails.getPassword(), 
                    userDetails.getAuthorities()
                );
            } catch (EntityNotFoundException exc) {
                throw new BadCredentialsException(exc.getMessage());
            }
        }
    }

Security Configuration:

The security rules are defined in SecurityConfig:

    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) 
                throws Exception {
            http
                .authorizeHttpRequests(
                    authorizeRequests ->
                        authorizeRequests
                            .requestMatchers("/", "/home", "/login", "/css/**", 
                                "/js/**", "/images/**", "/register", 
                                "/search-routes")
                            .permitAll()
                            .requestMatchers("/routes/company/**")
                                .hasAnyRole("TRANSPORT_ORGANIZER", "DRIVER")
                            .requestMatchers("/admin/**")
                                .hasRole("ADMIN")
                            .requestMatchers("/trips/user/**")
                                .hasRole("USER")
                            .anyRequest().authenticated()
                )
                .formLogin(login ->
                    login.loginPage("/login")
                        .permitAll()
                        .defaultSuccessUrl("/")
                        .failureUrl("/login?error")
                );
            return http.build();
        }
    }

Role Resolution:

User roles are determined through the RoleResolver utility:

    public class RoleResolver {
        public static Collection<? extends GrantedAuthority> resolveRoles(Account account) {
            List<GrantedAuthority> authorities = new ArrayList<>();

            if (account.getAdmin() != null) {
                authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            }
            if (account.getStudent() != null) {
                authorities.add(new SimpleGrantedAuthority("ROLE_STUDENT"));
            }
            if (account.getDriver() != null) {
                authorities.add(new SimpleGrantedAuthority("ROLE_DRIVER"));
            }
            if (account.getTransportOrganizer() != null) {
                authorities.add(new SimpleGrantedAuthority("ROLE_TRANSPORT_ORGANIZER"));
            }
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

            return authorities;
        }
    }

Result of /login page

Result of failed login attempt

ID-6 - Register

The /register endpoint provides user registration functionality, allowing new users to create accounts in the system. It supports basic user registration with name, surname, email, and password validation.

Registration Flow

The registration process follows these steps:

  1. User accesses the registration page: /register
  2. User fills out the registration form with required information
  3. System validates the input and creates a new account
  4. User is redirected to login page upon successful registration

    @Controller
    @RequestMapping("/register")
    public class RegisterController {
        private final AccountService accountService;

        @GetMapping
        public String registerPage(@RequestParam(required = false) String error,
                                   Model model) {
            if (error != null) {
                model.addAttribute("errorMessage", error);
            }
            model.addAttribute("display", "register");
            return "master";
        }

        @PostMapping
        private String registerUser(@RequestParam String name,
                                    @RequestParam String surname,
                                    @RequestParam String email,
                                    @RequestParam String password,
                                    @RequestParam String confirmPassword) {
            try {
                accountService.save(email, name, surname, password, confirmPassword);
            } catch (Exception e) {
                return String.format("redirect:/register?error=%s", e.getMessage());
            }
            return "redirect:/login";
        }
    }

Controller Details:

  • URL Mapping: /register
  • Methods:
    • GET: Displays registration form
    • POST: Processes registration request
  • Functionality:
    • Displays registration form with error handling
    • Processes user registration with field validation
    • Handles registration errors and displays appropriate messages
    • Redirects to login page upon successful registration
  • View: Uses the master template and dynamically embeds register view

Result of /register page

Result of /register with error

Attachments (15)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.