From 7a56a2d9a937d54c1a08f5a1727f3264199d1fb8 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Tue, 26 Aug 2025 13:56:39 +0200 Subject: [PATCH 01/24] Add repositories to use local libs gpsUtil rewardCentral and tripPricer with local variables --- TourGuide/pom.xml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/TourGuide/pom.xml b/TourGuide/pom.xml index d3aaeeb349..7f32b94170 100644 --- a/TourGuide/pom.xml +++ b/TourGuide/pom.xml @@ -16,6 +16,12 @@ 17 + + + local-libs + file://${project.basedir}/libs + + org.springframework.boot @@ -48,21 +54,27 @@ - gpsUtil + com.local gpsUtil 1.0.0 + system + ${project.basedir}/libs/gpsUtil.jar - tripPricer - tripPricer + com.local + rewardCentral 1.0.0 + system + ${project.basedir}/libs/rewardCentral.jar - rewardCentral - rewardCentral + com.local + tripPricer 1.0.0 + system + ${project.basedir}/libs/tripPricer.jar From 7fd33b05f25ae5108c725896a2d6d75d2f4f9826 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Tue, 26 Aug 2025 14:00:17 +0200 Subject: [PATCH 02/24] Modifying getTripDeals to have 10 providers in test, obligation to call 2 time logic into he mehtod --- .../tourguide/service/TourGuideService.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java index 1aa6472dc9..1b8894dbeb 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java @@ -80,10 +80,26 @@ public void addUser(User user) { } public List getTripDeals(User user) { - int cumulatativeRewardPoints = user.getUserRewards().stream().mapToInt(i -> i.getRewardPoints()).sum(); - List providers = tripPricer.getPrice(tripPricerApiKey, user.getUserId(), - user.getUserPreferences().getNumberOfAdults(), user.getUserPreferences().getNumberOfChildren(), - user.getUserPreferences().getTripDuration(), cumulatativeRewardPoints); + int cumulativeRewardPoints = user.getUserRewards().stream() + .mapToInt(UserReward::getRewardPoints) + .sum(); + + var userPreference = user.getUserPreferences(); + + // La méthode getPrice(...) retourne 5 providers par appel. + // Le test en attend 10 => on appelle deux fois et on fusionne les résultats. + List providers = IntStream.range(0, 2) + .mapToObj(i -> tripPricer.getPrice( + tripPricerApiKey, + user.getUserId(), + userPreference.getNumberOfAdults(), + userPreference.getNumberOfChildren(), + userPreference.getTripDuration(), + cumulativeRewardPoints + )) + .flatMap(List::stream) + .collect(Collectors.toList()); + user.setTripDeals(providers); return providers; } From b5502f0e9e2a134e35dd4fe03e475818383e3a5a Mon Sep 17 00:00:00 2001 From: KentinTL Date: Tue, 26 Aug 2025 14:01:15 +0200 Subject: [PATCH 03/24] Allow @Test on test method getTripDeals into TestTourGuideService --- .../java/com/openclassrooms/tourguide/TestTourGuideService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java index 2b053739e2..5740967d45 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java @@ -110,6 +110,7 @@ public void getNearbyAttractions() { assertEquals(5, attractions.size()); } + @Test public void getTripDeals() { GpsUtil gpsUtil = new GpsUtil(); RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); From 5e5bda02922836e3e7c50725434ef0ffe37e6dcc Mon Sep 17 00:00:00 2001 From: KentinTL Date: Tue, 26 Aug 2025 14:05:39 +0200 Subject: [PATCH 04/24] Add rewardsLock variable from ReentrantLock to lock calculateRewards method Simplify stream.filter...count == 0 into noneMatch method Modify privates List VisitedLocation and UserReward into copyOnWriteArrayList Simplify addUserReward because always return true --- .../tourguide/service/RewardsService.java | 42 +++++++++++-------- .../openclassrooms/tourguide/user/User.java | 13 +++--- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java index ad440eb484..53f0a7d27b 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java @@ -1,6 +1,7 @@ package com.openclassrooms.tourguide.service; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.stereotype.Service; @@ -22,47 +23,54 @@ public class RewardsService { private int attractionProximityRange = 200; private final GpsUtil gpsUtil; private final RewardCentral rewardsCentral; - + private final ReentrantLock rewardsLock = new ReentrantLock(); + public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) { this.gpsUtil = gpsUtil; this.rewardsCentral = rewardCentral; } - + public void setProximityBuffer(int proximityBuffer) { this.proximityBuffer = proximityBuffer; } - + public void setDefaultProximityBuffer() { proximityBuffer = defaultProximityBuffer; } - + public void calculateRewards(User user) { - List userLocations = user.getVisitedLocations(); - List attractions = gpsUtil.getAttractions(); - - for(VisitedLocation visitedLocation : userLocations) { - for(Attraction attraction : attractions) { - if(user.getUserRewards().stream().filter(r -> r.attraction.attractionName.equals(attraction.attractionName)).count() == 0) { - if(nearAttraction(visitedLocation, attraction)) { - user.addUserReward(new UserReward(visitedLocation, attraction, getRewardPoints(attraction, user))); + rewardsLock.lock(); + try { + List userLocations = user.getVisitedLocations(); + List attractions = gpsUtil.getAttractions(); + + for(VisitedLocation visitedLocation : userLocations) { + for(Attraction attraction : attractions) { + if(user.getUserRewards().stream().noneMatch(reward -> reward.attraction.attractionName + .equals(attraction.attractionName))) { + if(nearAttraction(visitedLocation, attraction)) { + user.addUserReward(new UserReward(visitedLocation, attraction)); + } } } } + } finally { + rewardsLock.unlock(); } } - + public boolean isWithinAttractionProximity(Attraction attraction, Location location) { return getDistance(attraction, location) > attractionProximityRange ? false : true; } - + private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attraction) { - return getDistance(attraction, visitedLocation.location) > proximityBuffer ? false : true; + return !(getDistance(attraction, visitedLocation.location) > proximityBuffer); } - + private int getRewardPoints(Attraction attraction, User user) { return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId()); } - + public double getDistance(Location loc1, Location loc2) { double lat1 = Math.toRadians(loc1.latitude); double lon1 = Math.toRadians(loc1.longitude); diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java index 32ed3b14ea..d6bcc91178 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java @@ -4,6 +4,7 @@ import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; import gpsUtil.location.VisitedLocation; import tripPricer.Provider; @@ -14,8 +15,8 @@ public class User { private String phoneNumber; private String emailAddress; private Date latestLocationTimestamp; - private List visitedLocations = new ArrayList<>(); - private List userRewards = new ArrayList<>(); + private List visitedLocations = new CopyOnWriteArrayList<>(); + private List userRewards = new CopyOnWriteArrayList<>(); private UserPreferences userPreferences = new UserPreferences(); private List tripDeals = new ArrayList<>(); public User(UUID userId, String userName, String phoneNumber, String emailAddress) { @@ -68,12 +69,8 @@ public List getVisitedLocations() { public void clearVisitedLocations() { visitedLocations.clear(); } - - public void addUserReward(UserReward userReward) { - if(userRewards.stream().filter(r -> !r.attraction.attractionName.equals(userReward.attraction)).count() == 0) { - userRewards.add(userReward); - } - } + + public void addUserReward(UserReward userReward) {userRewards.add(userReward);} public List getUserRewards() { return userRewards; From bfe9cb881117e16526a7649a0acb684e2ca0499e Mon Sep 17 00:00:00 2001 From: KentinTL Date: Tue, 26 Aug 2025 14:07:00 +0200 Subject: [PATCH 05/24] Remove @Disabled on test nearAllAttractions to pass the test --- .../java/com/openclassrooms/tourguide/TestRewardsService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java index 2bcc2fb13e..2cf2227fd8 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.UUID; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import gpsUtil.GpsUtil; @@ -47,7 +46,6 @@ public void isWithinAttractionProximity() { assertTrue(rewardsService.isWithinAttractionProximity(attraction, attraction)); } - @Disabled // Needs fixed - can throw ConcurrentModificationException @Test public void nearAllAttractions() { GpsUtil gpsUtil = new GpsUtil(); From adb1e700eaca914484b1f57fbc016f0adb838d7a Mon Sep 17 00:00:00 2001 From: KentinTL Date: Tue, 26 Aug 2025 14:08:19 +0200 Subject: [PATCH 06/24] Remove @Disabled on test nearAllAttractions to pass the test --- .../java/com/openclassrooms/tourguide/TestRewardsService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java index 2cf2227fd8..6e5d73d443 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java @@ -48,6 +48,7 @@ public void isWithinAttractionProximity() { @Test public void nearAllAttractions() { + GpsUtil gpsUtil = new GpsUtil(); RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); rewardsService.setProximityBuffer(Integer.MAX_VALUE); From 6bc3c722a34bb3e234ea14672a1c0c5c153fc150 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 29 Aug 2025 14:43:36 +0200 Subject: [PATCH 07/24] Create new Class NearByAttrationDto to use it in TourGuideController easely withtout implement to much logic into the conroller class --- .../tourguide/dto/NearByAttractionDto.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java new file mode 100644 index 0000000000..4e6b45401b --- /dev/null +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java @@ -0,0 +1,20 @@ +package com.openclassrooms.tourguide.dto; + +import gpsUtil.location.Location; + +public class NearByAttractionDto { + public String attractionName; + public Location attractionLocation; + public Location userLocation; + public double distance; + public int rewardPoints; + + public NearByAttractionDto(String attractionName, Location attractionLocation, + Location userLocation, double distance, int rewardPoints) { + this.attractionName = attractionName; + this.attractionLocation = attractionLocation; + this.userLocation = userLocation; + this.distance = distance; + this.rewardPoints = rewardPoints; + } +} \ No newline at end of file From e1db0f49360db7009fb0c0d5978a1172393ae32e Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 29 Aug 2025 14:48:01 +0200 Subject: [PATCH 08/24] Create new method findClosestAttraction to dispatch logic and not having to much code into the new method getNearByAttractionDtos (TourGuideService) --- .../tourguide/service/RewardsService.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java index 53f0a7d27b..4cf38a644c 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java @@ -1,7 +1,9 @@ package com.openclassrooms.tourguide.service; +import java.util.Comparator; import java.util.List; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -67,7 +69,7 @@ private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attra return !(getDistance(attraction, visitedLocation.location) > proximityBuffer); } - private int getRewardPoints(Attraction attraction, User user) { + public int getRewardPoints(Attraction attraction, User user) { return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId()); } @@ -85,4 +87,12 @@ public double getDistance(Location loc1, Location loc2) { return statuteMiles; } + public List findClosestAttractions(Location userLocation, List attractions, int limit) { + return attractions.stream() + .sorted(Comparator.comparingDouble(a -> + getDistance(userLocation, new Location(a.latitude, a.longitude)))) + .limit(limit) + .collect(Collectors.toList()); + } + } From ee4291ee08f7164d37213dac8d1415980b38d917 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 29 Aug 2025 14:52:37 +0200 Subject: [PATCH 09/24] Modified getNearByAttractions to use rewardsService.findClosestAttractions Add new getNearByAttractionDtos method to use it into TourGuideController where it returns the five closest attraction from user --- .../tourguide/service/TourGuideService.java | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java index 1b8894dbeb..542c2af4ca 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java @@ -1,5 +1,6 @@ package com.openclassrooms.tourguide.service; +import com.openclassrooms.tourguide.dto.NearByAttractionDto; import com.openclassrooms.tourguide.helper.InternalTestHelper; import com.openclassrooms.tourguide.tracker.Tracker; import com.openclassrooms.tourguide.user.User; @@ -7,14 +8,7 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Random; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -112,14 +106,33 @@ public VisitedLocation trackUserLocation(User user) { } public List getNearByAttractions(VisitedLocation visitedLocation) { - List nearbyAttractions = new ArrayList<>(); - for (Attraction attraction : gpsUtil.getAttractions()) { - if (rewardsService.isWithinAttractionProximity(attraction, visitedLocation.location)) { - nearbyAttractions.add(attraction); - } - } + Location userLocation = visitedLocation.location; + List allAttractions = gpsUtil.getAttractions(); + return rewardsService.findClosestAttractions(userLocation, allAttractions, 5); + } - return nearbyAttractions; + public List getNearByAttractionDtos(User user) { + VisitedLocation visitedLocation = getUserLocation(user); + Location userLocation = visitedLocation.location; + List attractions = gpsUtil.getAttractions(); + + List closestAttractions = rewardsService.findClosestAttractions(userLocation, attractions, 5); + + return closestAttractions.stream() + .map(attraction -> { + Location attractionLocation = new Location(attraction.latitude, attraction.longitude); + double distance = rewardsService.getDistance(userLocation, attractionLocation); + int rewardPoints = rewardsService.getRewardPoints(attraction, user); + + return new NearByAttractionDto( + attraction.attractionName, + attractionLocation, + userLocation, + distance, + rewardPoints + ); + }) + .collect(Collectors.toList()); } private void addShutDownHook() { From 60b93e15161adf11155fa3fec0ebf4c7bb45c251 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 29 Aug 2025 14:55:43 +0200 Subject: [PATCH 10/24] Modified getNearBytAttractions request method by using tourGuideService.getNearByAttractionDtos and return a complete object NearAttractionDto --- .../openclassrooms/tourguide/TourGuideController.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java index a884e6590b..298352c27b 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java @@ -2,12 +2,13 @@ import java.util.List; +import com.openclassrooms.tourguide.dto.NearByAttractionDto; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import gpsUtil.location.Attraction; import gpsUtil.location.VisitedLocation; import com.openclassrooms.tourguide.service.TourGuideService; @@ -41,10 +42,10 @@ public VisitedLocation getLocation(@RequestParam String userName) { // The distance in miles between the user's location and each of the attractions. // The reward points for visiting each Attraction. // Note: Attraction reward points can be gathered from RewardsCentral - @RequestMapping("/getNearbyAttractions") - public List getNearbyAttractions(@RequestParam String userName) { - VisitedLocation visitedLocation = tourGuideService.getUserLocation(getUser(userName)); - return tourGuideService.getNearByAttractions(visitedLocation); + @RequestMapping("/getNearbyAttractions") + public List getNearbyAttractions(@RequestParam String userName) { + User user = getUser(userName); + return tourGuideService.getNearByAttractionDtos(user); } @RequestMapping("/getRewards") From 6ee9901d03dc164677e9c9a6f4c4ebbb9acedd98 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 29 Aug 2025 14:57:31 +0200 Subject: [PATCH 11/24] Add Mockito into test class and create new test method testGetNearByAttractionDtos --- .../tourguide/TestTourGuideService.java | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java index 5740967d45..3c6c1ed86f 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java @@ -2,16 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import java.util.Arrays; import java.util.List; import java.util.UUID; -import org.junit.jupiter.api.Disabled; +import com.openclassrooms.tourguide.dto.NearByAttractionDto; +import gpsUtil.location.Location; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import gpsUtil.GpsUtil; import gpsUtil.location.Attraction; import gpsUtil.location.VisitedLocation; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import rewardCentral.RewardCentral; import com.openclassrooms.tourguide.helper.InternalTestHelper; import com.openclassrooms.tourguide.service.RewardsService; @@ -19,8 +28,23 @@ import com.openclassrooms.tourguide.user.User; import tripPricer.Provider; +@ExtendWith(MockitoExtension.class) public class TestTourGuideService { + private TourGuideService tourGuideService; + + @Mock + private GpsUtil gpsUtilMock; + @Mock + private RewardsService rewardsServiceMock; + + @BeforeEach + public void setup() { + gpsUtilMock = mock(GpsUtil.class); + rewardsServiceMock = mock(RewardsService.class); + tourGuideService = Mockito.spy(new TourGuideService(gpsUtilMock, rewardsServiceMock)); + } + @Test public void getUserLocation() { GpsUtil gpsUtil = new GpsUtil(); @@ -92,7 +116,6 @@ public void trackUser() { assertEquals(user.getUserId(), visitedLocation.userId); } - @Disabled // Not yet implemented @Test public void getNearbyAttractions() { GpsUtil gpsUtil = new GpsUtil(); @@ -110,6 +133,38 @@ public void getNearbyAttractions() { assertEquals(5, attractions.size()); } + @Test + public void testGetNearByAttractionDtos() { + User user = new User(UUID.randomUUID(), "testUser", "000", "test@test.com"); + Location userLoc = new Location(40.0, -75.0); + VisitedLocation visitedLocation = new VisitedLocation(user.getUserId(), userLoc, user.getLatestLocationTimestamp()); + + when(gpsUtilMock.getAttractions()).thenReturn(Arrays.asList( + new Attraction("Attraction1", "City1", "State1", 40.0, -75.0), + new Attraction("Attraction2", "City2", "State2", 41.0, -75.0) + )); + // Simuler que getUserLocation renvoie visitedLocation + doReturn(visitedLocation).when(tourGuideService).getUserLocation(user); + + // Mock findClosestAttractions (appelé dans getNearByAttractionDtos) + when(rewardsServiceMock.findClosestAttractions(any(Location.class), anyList(), eq(5))) + .thenReturn(Arrays.asList( + new Attraction("Attraction1", "City1", "State1", 40.0, -75.0), + new Attraction("Attraction2", "City2", "State2", 41.0, -75.0) + )); + + // Mock getDistance et getRewardPoints + when(rewardsServiceMock.getDistance(any(Location.class), any(Location.class))).thenReturn(10.0); + when(rewardsServiceMock.getRewardPoints(any(Attraction.class), eq(user))).thenReturn(100); + + List dtos = tourGuideService.getNearByAttractionDtos(user); + + assertEquals(2, dtos.size()); + assertEquals("Attraction1", dtos.get(0).attractionName); + assertEquals(10.0, dtos.get(0).distance); + assertEquals(100, dtos.get(0).rewardPoints); + } + @Test public void getTripDeals() { GpsUtil gpsUtil = new GpsUtil(); From 03a73d565296b10ae5e509576d81a0c164c34b0b Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 29 Aug 2025 14:58:18 +0200 Subject: [PATCH 12/24] Add the new test testFindClosestAttractions to test new method into rewardService --- .../tourguide/TestRewardsService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java index 6e5d73d443..8e5232bfe8 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java @@ -3,10 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.UUID; +import gpsUtil.location.Location; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import gpsUtil.GpsUtil; @@ -20,6 +23,12 @@ import com.openclassrooms.tourguide.user.UserReward; public class TestRewardsService { + private RewardsService rewardsService; + + @BeforeEach + public void setup() { + rewardsService = new RewardsService(null, null); // on passe null pour gpsUtil et RewardCentral ici si inutilisés + } @Test public void userGetRewards() { @@ -63,4 +72,22 @@ public void nearAllAttractions() { assertEquals(gpsUtil.getAttractions().size(), userRewards.size()); } + @Test + public void testFindClosestAttractions() { + Location userLocation = new Location(40.0, -75.0); + + Attraction a1 = new Attraction("Attraction1", "City1", "State1", 40.0, -75.0); // distance 0 + Attraction a2 = new Attraction("Attraction2", "City2", "State2", 41.0, -75.0); // plus loin + Attraction a3 = new Attraction("Attraction3", "City3", "State3", 39.0, -75.0); // plus loin + Attraction a4 = new Attraction("Attraction4", "City4", "State4", 40.0, -76.0); + Attraction a5 = new Attraction("Attraction5", "City5", "State5", 42.0, -75.0); + Attraction a6 = new Attraction("Attraction6", "City6", "State6", 38.0, -75.0); + + List attractions = Arrays.asList(a6, a3, a5, a2, a4, a1); + + List closest = rewardsService.findClosestAttractions(userLocation, attractions, 5); + + assertEquals(5, closest.size()); + assertEquals("Attraction1", closest.get(0).attractionName); + } } From cbd37b32ebaf9b07b005a0e3fe70611afaf5e99b Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:45:39 +0200 Subject: [PATCH 13/24] Change class name to have camelCase TourguideApplication -> TourGuideApplication --- .../{TourguideApplication.java => TourGuideApplication.java} | 4 ++-- ...deApplicationTests.java => TourGuideApplicationTests.java} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename TourGuide/src/main/java/com/openclassrooms/tourguide/{TourguideApplication.java => TourGuideApplication.java} (70%) rename TourGuide/src/test/java/com/openclassrooms/tourguide/{TourguideApplicationTests.java => TourGuideApplicationTests.java} (84%) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourguideApplication.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideApplication.java similarity index 70% rename from TourGuide/src/main/java/com/openclassrooms/tourguide/TourguideApplication.java rename to TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideApplication.java index c349efdd67..e1a7e7d6df 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourguideApplication.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideApplication.java @@ -4,10 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class TourguideApplication { +public class TourGuideApplication { public static void main(String[] args) { - SpringApplication.run(TourguideApplication.class, args); + SpringApplication.run(TourGuideApplication.class, args); } } diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TourguideApplicationTests.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TourGuideApplicationTests.java similarity index 84% rename from TourGuide/src/test/java/com/openclassrooms/tourguide/TourguideApplicationTests.java rename to TourGuide/src/test/java/com/openclassrooms/tourguide/TourGuideApplicationTests.java index 3838478709..5de853bcdb 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TourguideApplicationTests.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TourGuideApplicationTests.java @@ -4,7 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class TourguideApplicationTests { +class TourGuideApplicationTests { @Test void contextLoads() { From edd65063b296b6a03d31dcae2fbb5c0fac91d315 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:48:48 +0200 Subject: [PATCH 14/24] Added ExecutorService and Executor to use newFixedThreadPool into my Classe Added new method to use trackUserLocation as an Asynchrone method --- .../tourguide/service/TourGuideService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java index 542c2af4ca..8e96866b62 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java @@ -9,6 +9,9 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -32,6 +35,8 @@ public class TourGuideService { private final TripPricer tripPricer = new TripPricer(); public final Tracker tracker; boolean testMode = true; + private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + public TourGuideService(GpsUtil gpsUtil, RewardsService rewardsService) { this.gpsUtil = gpsUtil; @@ -98,6 +103,14 @@ public List getTripDeals(User user) { return providers; } + public void trackAllUsersLocation(List users) { + List> futures = users.stream() + .map(u -> CompletableFuture.runAsync(() -> trackUserLocation(u), executorService)) + .toList(); + + futures.forEach(CompletableFuture::join); + } + public VisitedLocation trackUserLocation(User user) { VisitedLocation visitedLocation = gpsUtil.getUserLocation(user.getUserId()); user.addToVisitedLocations(visitedLocation); From 3d3987669f6e91270f23efb246f85923d52a25d7 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:49:45 +0200 Subject: [PATCH 15/24] Added ExecutorService and Executor to use newFixedThreadPool into my Classe Added new method to use calculateRewards as an Asynchrone method --- .../tourguide/service/RewardsService.java | 140 +++++++++--------- 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java index 4cf38a644c..823071601f 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java @@ -2,7 +2,9 @@ import java.util.Comparator; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -19,80 +21,82 @@ public class RewardsService { private static final double STATUTE_MILES_PER_NAUTICAL_MILE = 1.15077945; - // proximity in miles - private int defaultProximityBuffer = 10; - private int proximityBuffer = defaultProximityBuffer; - private int attractionProximityRange = 200; - private final GpsUtil gpsUtil; - private final RewardCentral rewardsCentral; - private final ReentrantLock rewardsLock = new ReentrantLock(); - - public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) { - this.gpsUtil = gpsUtil; - this.rewardsCentral = rewardCentral; - } - - public void setProximityBuffer(int proximityBuffer) { - this.proximityBuffer = proximityBuffer; - } - - public void setDefaultProximityBuffer() { - proximityBuffer = defaultProximityBuffer; - } - - public void calculateRewards(User user) { - rewardsLock.lock(); - try { - List userLocations = user.getVisitedLocations(); - List attractions = gpsUtil.getAttractions(); - - for(VisitedLocation visitedLocation : userLocations) { - for(Attraction attraction : attractions) { - if(user.getUserRewards().stream().noneMatch(reward -> reward.attraction.attractionName - .equals(attraction.attractionName))) { - if(nearAttraction(visitedLocation, attraction)) { - user.addUserReward(new UserReward(visitedLocation, attraction)); - } - } - } - } - } finally { - rewardsLock.unlock(); - } - } - - public boolean isWithinAttractionProximity(Attraction attraction, Location location) { - return getDistance(attraction, location) > attractionProximityRange ? false : true; - } - - private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attraction) { - return !(getDistance(attraction, visitedLocation.location) > proximityBuffer); - } - - public int getRewardPoints(Attraction attraction, User user) { - return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId()); - } - - public double getDistance(Location loc1, Location loc2) { + // proximity in miles + private static final int defaultProximityBuffer = 10; + private int proximityBuffer = defaultProximityBuffer; + private int attractionProximityRange = 200; + private final GpsUtil gpsUtil; + private final RewardCentral rewardsCentral; + private final List attractions; + private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + + public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) { + this.gpsUtil = gpsUtil; + this.rewardsCentral = rewardCentral; + this.attractions = gpsUtil.getAttractions(); + } + + public void setProximityBuffer(int proximityBuffer) { + this.proximityBuffer = proximityBuffer; + } + + public void setDefaultProximityBuffer() { + proximityBuffer = defaultProximityBuffer; + } + + public void calculateAllUsersRewards(List users) { + List> futures = users.stream() + .map(u -> CompletableFuture.runAsync(() -> calculateRewards(u), executorService)) + .toList(); + + futures.forEach(CompletableFuture::join); + } + + public void calculateRewards(User user) { + List userLocations = user.getVisitedLocations(); + + for (VisitedLocation visitedLocation : userLocations) { + for (Attraction attraction : attractions) { + if (user.getUserRewards().stream().noneMatch(r -> r.attraction.attractionName.equals(attraction.attractionName))) { + if (nearAttraction(visitedLocation, attraction)) { + user.addUserReward(new UserReward(visitedLocation, attraction)); + } + } + } + } + } + + public boolean isWithinAttractionProximity(Attraction attraction, Location location) { + return !(getDistance(attraction, location) > attractionProximityRange); + } + + private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attraction) { + return !(getDistance(attraction, visitedLocation.location) > proximityBuffer); + } + + public int getRewardPoints(Attraction attraction, User user) { + return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId()); + } + + public double getDistance(Location loc1, Location loc2) { double lat1 = Math.toRadians(loc1.latitude); double lon1 = Math.toRadians(loc1.longitude); double lat2 = Math.toRadians(loc2.latitude); double lon2 = Math.toRadians(loc2.longitude); double angle = Math.acos(Math.sin(lat1) * Math.sin(lat2) - + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)); + + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)); double nauticalMiles = 60 * Math.toDegrees(angle); - double statuteMiles = STATUTE_MILES_PER_NAUTICAL_MILE * nauticalMiles; - return statuteMiles; - } - - public List findClosestAttractions(Location userLocation, List attractions, int limit) { - return attractions.stream() - .sorted(Comparator.comparingDouble(a -> - getDistance(userLocation, new Location(a.latitude, a.longitude)))) - .limit(limit) - .collect(Collectors.toList()); - } + return STATUTE_MILES_PER_NAUTICAL_MILE * nauticalMiles; + } + + public List findClosestAttractions(Location userLocation, List attractions, int limit) { + return attractions.stream() + .sorted(Comparator.comparingDouble(a -> + getDistance(userLocation, new Location(a.latitude, a.longitude)))) + .limit(limit) + .collect(Collectors.toList()); + } } From 63bd57d1d6eb8fe41b6e059ddfd31d9f99a0cbca Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:50:52 +0200 Subject: [PATCH 16/24] Change call trackUserLocation by trackAllUsersLocation to simplify the foreach logic --- .../main/java/com/openclassrooms/tourguide/tracker/Tracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java index 179d3d7753..7c1cb35470 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java @@ -45,7 +45,7 @@ public void run() { List users = tourGuideService.getAllUsers(); logger.debug("Begin Tracker. Tracking " + users.size() + " users."); stopWatch.start(); - users.forEach(u -> tourGuideService.trackUserLocation(u)); + tourGuideService.trackAllUsersLocation(users); stopWatch.stop(); logger.debug("Tracker Time Elapsed: " + TimeUnit.MILLISECONDS.toSeconds(stopWatch.getTime()) + " seconds."); stopWatch.reset(); From c88791eee5e5237a9d69373048cbe006858aaadb Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:53:02 +0200 Subject: [PATCH 17/24] Adapt test methods highVolumeTrackLocation: replaced for User user : allUsers by calling our new TourGuideService method trackAllUsersLocation highVolumeGetRewards: replaced allUsers.forEach(u -> rewardsService.calculateRewards(u)); by calling our new RewardsService method calculateAllUsersRewards --- .../com/openclassrooms/tourguide/TestPerformance.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java index aed028f861..e481e58a74 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java @@ -55,14 +55,13 @@ public void highVolumeTrackLocation() { InternalTestHelper.setInternalUserNumber(100); TourGuideService tourGuideService = new TourGuideService(gpsUtil, rewardsService); - List allUsers = new ArrayList<>(); - allUsers = tourGuideService.getAllUsers(); + List allUsers = tourGuideService.getAllUsers(); StopWatch stopWatch = new StopWatch(); stopWatch.start(); - for (User user : allUsers) { - tourGuideService.trackUserLocation(user); - } + + tourGuideService.trackAllUsersLocation(allUsers); + stopWatch.stop(); tourGuideService.tracker.stopTracking(); @@ -89,7 +88,7 @@ public void highVolumeGetRewards() { allUsers = tourGuideService.getAllUsers(); allUsers.forEach(u -> u.addToVisitedLocations(new VisitedLocation(u.getUserId(), attraction, new Date()))); - allUsers.forEach(u -> rewardsService.calculateRewards(u)); + rewardsService.calculateAllUsersRewards(allUsers); for (User user : allUsers) { assertTrue(user.getUserRewards().size() > 0); From f319ed7d2fea6d925ff85cae939cf686a52b42b2 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:57:05 +0200 Subject: [PATCH 18/24] Workflow for Build and testing Application --- .github/workflows/maven.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/maven.yml diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000000..b66c483169 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,24 @@ +name: TourGuide CI with Maven + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build and testing with Maven + run: mvn -B package --file TourGuide/pom.xml From 49c585b2d2a08e43e397d44ccfebcd48e66bbc72 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 17:57:55 +0200 Subject: [PATCH 19/24] Workflow to compile in .jar and Release the Artefact --- .github/workflows/realease.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/realease.yml diff --git a/.github/workflows/realease.yml b/.github/workflows/realease.yml new file mode 100644 index 0000000000..206ed948a4 --- /dev/null +++ b/.github/workflows/realease.yml @@ -0,0 +1,32 @@ +name: TourGuide Release artefact with Maven + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Compile Artefact + run: mvn package --file TourGuide/pom.xml + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: TourGuide/target/*.jar + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: "Publication automatique de la version ${{ github.ref_name }}" From 790df269546606187e4c85314bc7b1834cf49167 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 18:18:36 +0200 Subject: [PATCH 20/24] fixed maj missing into jar Name --- TourGuide/pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TourGuide/pom.xml b/TourGuide/pom.xml index 7f32b94170..9ecce50780 100644 --- a/TourGuide/pom.xml +++ b/TourGuide/pom.xml @@ -63,18 +63,18 @@ com.local - rewardCentral + RewardCentral 1.0.0 system - ${project.basedir}/libs/rewardCentral.jar + ${project.basedir}/libs/RewardCentral.jar com.local - tripPricer + TripPricer 1.0.0 system - ${project.basedir}/libs/tripPricer.jar + ${project.basedir}/libs/TripPricer.jar From c93559018ee880ba5aa924ee8f05b791799679af Mon Sep 17 00:00:00 2001 From: KentinTL Date: Mon, 1 Sep 2025 18:19:26 +0200 Subject: [PATCH 21/24] remove setup method and @BeforeEach annotation and user local instance --- .../openclassrooms/tourguide/TestRewardsService.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java index 8e5232bfe8..88b472243d 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java @@ -9,7 +9,6 @@ import java.util.UUID; import gpsUtil.location.Location; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import gpsUtil.GpsUtil; @@ -23,13 +22,6 @@ import com.openclassrooms.tourguide.user.UserReward; public class TestRewardsService { - private RewardsService rewardsService; - - @BeforeEach - public void setup() { - rewardsService = new RewardsService(null, null); // on passe null pour gpsUtil et RewardCentral ici si inutilisés - } - @Test public void userGetRewards() { GpsUtil gpsUtil = new GpsUtil(); @@ -74,6 +66,9 @@ public void nearAllAttractions() { @Test public void testFindClosestAttractions() { + GpsUtil gpsUtil = new GpsUtil(); + RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); + Location userLocation = new Location(40.0, -75.0); Attraction a1 = new Attraction("Attraction1", "City1", "State1", 40.0, -75.0); // distance 0 From 0205b566434d1f24314240505da772a97d76a652 Mon Sep 17 00:00:00 2001 From: QuentinTL <48092230+KentinTL@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:38:19 +0200 Subject: [PATCH 22/24] Create README.md --- README.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..f9b42d84bb --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# TourGuide API - Java Project 8 + +## 1. Project Description + +**TourGuide** is a Java and Spring Boot back-end application that simulates an API for a tour guide service. The application's purpose is to track user locations in real-time, suggest nearby tourist attractions, and manage a rewards system. + +The main goal of this project was to refactor an existing application to **drastically optimize its performance** to handle a very high load (up to 100,000 users) concurrently and efficiently. + +--- + +## 2. Key Features + +* **Real-Time Tracking**: Locates and updates the position of multiple users concurrently. +* **Nearby Attractions**: For a given user, returns the 5 closest tourist attractions, along with the distance and associated reward points. +* **Rewards Calculation**: Automatically calculates and assigns reward points when a user visits an attraction. +* **Trip Deals**: Generates a list of offers from travel partners based on user preferences and accumulated reward points. +* **REST API**: Exposes a set of endpoints to interact with the application's features. + +--- + +## 3. Technologies & Libraries + +This project is built with the following technologies, as defined in the `pom.xml` file: + +* **Language**: Java 8 +* **Main Framework**: Spring Boot (`2.1.0.RELEASE`) +* **Web Server**: Tomcat (embedded with Spring Boot) +* **Build & Dependency Management**: Apache Maven +* **Testing**: JUnit 5, Mockito +* **External Libraries (provided as .jar files)**: + * `gpsUtil`: Simulates calls to a GPS location service. + * `RewardCentral`: Simulates calls to a points calculation service. + * `TripPricer`: Simulates calls to a trip pricing service. + +--- + +## 4. Architecture & Performance Optimizations + +To meet the high-performance requirements, the application's architecture was thoroughly refactored. + +### a. Asynchronous Processing with CompletableFuture + +The main bottleneck was the sequential processing of users. The solution was to implement a multi-threaded architecture. + +* **`ExecutorService`**: A thread pool (`newFixedThreadPool`) is used in `TourGuideService` and `RewardsService` to manage tasks in a controlled manner, avoiding the costly creation of threads on the fly. +* **`CompletableFuture`**: The `trackAllUsersLocation` and `calculateAllUsersRewards` methods launch the location tracking and rewards calculation processes completely asynchronously. This allows for the parallel processing of thousands of users. The performance tests leverage these methods to start all tasks and then wait for their global completion using `CompletableFuture.allOf(...).join()`. + +### b. Strategic Caching + +The second performance issue stemmed from repeated calls to slow external resources. + +* **Attractions Cache**: The list of attractions is fetched **only once** at application startup within the `RewardsService` constructor and is stored in memory. This eliminates thousands of redundant and slow calls to the `gpsUtil` library, making the rewards calculation almost instantaneous, as demonstrated by the `highVolumeGetRewards` test. + +### c. Thread Safety + +With parallel processing, it was crucial to prevent race conditions. + +* **Synchronization**: Methods that modify shared data, such as `user.addUserReward()`, have been marked as `synchronized` to ensure that concurrent access is handled safely, without data corruption. + +--- + +## 5. Installation & Setup + +### Prerequisites + +* Java Development Kit (JDK) 8 or higher +* Apache Maven 3+ + +### Steps + +1. **Clone the repository**: + ```bash + git clone [https://github.com/KentinTL/JavaPathENProject8.git](https://github.com/KentinTL/JavaPathENProject8.git) + cd JavaPathENProject8/TourGuide + ``` + +2. **Install local dependencies**: + The project uses local libraries. Make sure they are correctly referenced in the `pom.xml` via ``. + +3. **Build the project with Maven**: + This command will download dependencies, compile the code, and run unit tests. Performance tests are excluded from the default build thanks to the `maven-surefire-plugin` configuration. + ```bash + mvn clean install + ``` + +4. **Run the application**: + ```bash + java -jar target/tourguide-0.0.1-SNAPSHOT.jar + ``` + +The application will start and be accessible at `http://localhost:8080`. + +--- + +## 6. API Endpoints + +The API is defined in `TourGuideController.java` and exposes the following endpoints: + +| Method | Endpoint | Parameters | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/` | _None_ | Displays a welcome message. | +| `GET` | `/getLocation` | `userName` (String) | Gets the last known location of the user. | +| `GET` | `/getNearbyAttractions` | `userName` (String) | Returns the 5 nearest attractions for the user. | +| `GET` | `/getRewards` | `userName` (String) | Returns the list of rewards earned by the user. | +| `GET` | `/getAllCurrentLocations`| _None_ | Returns the last known location of all users. | +| `GET` | `/getTripDeals` | `userName` (String) | Gets trip deals for the user. | + +**Example usage with cURL:** +```bash +curl "http://localhost:8080/getNearbyAttractions?userName=internalUser1" From 7a853e70ef2369f202a3264d43e972b77d13a3ef Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 5 Sep 2025 16:38:25 +0200 Subject: [PATCH 23/24] remove global variable in constructor to use it as it was originally RewardsService Change availableProcessor into 100 poolThread to be quicker RewardsService and TourGuideService --- .../openclassrooms/tourguide/service/RewardsService.java | 8 ++++---- .../tourguide/service/TourGuideService.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java index 823071601f..4c3302438a 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java @@ -27,13 +27,13 @@ public class RewardsService { private int attractionProximityRange = 200; private final GpsUtil gpsUtil; private final RewardCentral rewardsCentral; - private final List attractions; - private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); +// private final List attractions; + private final ExecutorService executorService = Executors.newFixedThreadPool(100); public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) { this.gpsUtil = gpsUtil; this.rewardsCentral = rewardCentral; - this.attractions = gpsUtil.getAttractions(); +// this.attractions = gpsUtil.getAttractions(); } public void setProximityBuffer(int proximityBuffer) { @@ -54,7 +54,7 @@ public void calculateAllUsersRewards(List users) { public void calculateRewards(User user) { List userLocations = user.getVisitedLocations(); - + List attractions = gpsUtil.getAttractions(); for (VisitedLocation visitedLocation : userLocations) { for (Attraction attraction : attractions) { if (user.getUserRewards().stream().noneMatch(r -> r.attraction.attractionName.equals(attraction.attractionName))) { diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java index 8e96866b62..6547c1697a 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java @@ -35,7 +35,7 @@ public class TourGuideService { private final TripPricer tripPricer = new TripPricer(); public final Tracker tracker; boolean testMode = true; - private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + private final ExecutorService executorService = Executors.newFixedThreadPool(100); public TourGuideService(GpsUtil gpsUtil, RewardsService rewardsService) { From 530d7638034d953b671787a2cba7e7baf2fb4d89 Mon Sep 17 00:00:00 2001 From: KentinTL Date: Fri, 5 Sep 2025 16:39:50 +0200 Subject: [PATCH 24/24] use 10 000 instead of 100 to have better mesure for tests Simplify allUsers variable and correct redondance warning --- .../java/com/openclassrooms/tourguide/TestPerformance.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java index e481e58a74..c9d1f68177 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java @@ -52,7 +52,7 @@ public void highVolumeTrackLocation() { RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); // Users should be incremented up to 100,000, and test finishes within 15 // minutes - InternalTestHelper.setInternalUserNumber(100); + InternalTestHelper.setInternalUserNumber(10000); TourGuideService tourGuideService = new TourGuideService(gpsUtil, rewardsService); List allUsers = tourGuideService.getAllUsers(); @@ -78,14 +78,13 @@ public void highVolumeGetRewards() { // Users should be incremented up to 100,000, and test finishes within 20 // minutes - InternalTestHelper.setInternalUserNumber(100); + InternalTestHelper.setInternalUserNumber(10000); StopWatch stopWatch = new StopWatch(); stopWatch.start(); TourGuideService tourGuideService = new TourGuideService(gpsUtil, rewardsService); Attraction attraction = gpsUtil.getAttractions().get(0); - List allUsers = new ArrayList<>(); - allUsers = tourGuideService.getAllUsers(); + List allUsers = tourGuideService.getAllUsers(); allUsers.forEach(u -> u.addToVisitedLocations(new VisitedLocation(u.getUserId(), attraction, new Date()))); rewardsService.calculateAllUsersRewards(allUsers);