Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: TourGuide CI

on:
push:
branches: [dev, master]
pull_request:
branches: [master]
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./TourGuide

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'

- name: Install local JARs to Maven repository
run: |
mvn install:install-file -Dfile=libs/gpsUtil.jar -DgroupId=gpsUtil -DartifactId=gpsUtil -Dversion=1.0.0 -Dpackaging=jar
mvn install:install-file -Dfile=libs/TripPricer.jar -DgroupId=tripPricer -DartifactId=tripPricer -Dversion=1.0.0 -Dpackaging=jar
mvn install:install-file -Dfile=libs/RewardCentral.jar -DgroupId=rewardCentral -DartifactId=rewardCentral -Dversion=1.0.0 -Dpackaging=jar

- name: Test
run: mvn -B test

- name: Package (skip tests, already run)
run: mvn -B package -DskipTests

- name: Upload JAR artifact
uses: actions/upload-artifact@v4
with:
name: tourguide-jar
path: TourGuide/target/*.jar
retention-days: 30
43 changes: 43 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# =====================================================
# .gitignore — TourGuide (OpenClassrooms Java Projet 8)
# =====================================================

# --- Fichiers locaux non versionnés ---
docs/

# --- Artefacts de build Maven ---
target/
build/
*.class

# --- IDE : IntelliJ IDEA ---
.idea/
*.iml
*.iws
*.ipr
out/

# --- IDE : VS Code ---
.vscode/

# --- IDE : Eclipse ---
.classpath
.project
.settings/
bin/

# --- OS : Windows / macOS / Linux ---
Thumbs.db
Desktop.ini
.DS_Store
*.swp
*.swo

# --- Logs ---
*.log
logs/

# --- Dépendances locales : NE PAS IGNORER ---
# Les JARs dans TourGuide/libs/ doivent être commités
# (gpsUtil, RewardCentral, TripPricer)
# Donc on N'AJOUTE PAS *.jar à ce gitignore.
129 changes: 129 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# TourGuide

![CI](https://github.com/Xajah/JavaPathENProject8/actions/workflows/build.yml/badge.svg)

Application Spring Boot de planification de voyages personnalisés, reprise et améliorée dans le cadre du **Projet 8 du parcours Java d'OpenClassrooms** (correction de bugs, optimisation des performances, mise en place d'un pipeline d'intégration continue).

---

## Contexte fonctionnel

TourGuide propose à ses utilisateurs des attractions touristiques à proximité, ainsi que des forfaits de voyage associés. Chaque action de l'utilisateur lui permet d'accumuler des points de fidélité auprès des partenaires.

Le projet original présentait trois problèmes que j'ai eu à traiter :
- des tests instables liés à des accès concurrents non protégés,
- un bug fonctionnel dans la recommandation d'attractions,
- des performances insuffisantes sur les opérations de masse (100 000 utilisateurs).

Ce dépôt contient la version corrigée.

---

## Architecture technique

- Java 17, Spring Boot 3.1.1, Maven
- Bibliothèques métier locales (non publiées sur Maven Central) : `gpsUtil`, `RewardCentral`, `TripPricer`, fournies dans `TourGuide/libs/`
- Tests : JUnit 5
- CI : GitHub Actions

Côté composants :

- `TourGuideController` expose les endpoints REST.
- `TourGuideService` orchestre la logique principale et délègue les appels lents à un pool de threads dédié pour la récupération des localisations (`gpsUtil`).
- `RewardsService` calcule les points de fidélité et utilise son propre pool de threads pour les appels à `RewardCentral`.
- Les collections internes de l'utilisateur (`visitedLocations`, `userRewards`) sont des `CopyOnWriteArrayList` pour rester sûres en accès concurrent.

---

## Prérequis

- JDK 17 ou supérieur
- Maven 3.9+

J'ai personnellement testé le build avec Eclipse Temurin 17 (utilisé par la CI) et Oracle JDK 23 (en local).

---

## Build et exécution

Toutes les commandes Maven se lancent depuis le dossier `TourGuide/`.

### Build complet (avec tests)

```bash
cd TourGuide
mvn clean package
```

Le JAR exécutable est généré dans `TourGuide/target/tourguide-0.0.1-SNAPSHOT.jar`.

### Lancement de l'application

```bash
java -jar TourGuide/target/tourguide-0.0.1-SNAPSHOT.jar
```

L'application est ensuite accessible sur `http://localhost:8080`.

### Endpoints REST principaux

| Endpoint | Description |
|---|---|
| `GET /getLocation?userName=<user>` | Dernière localisation connue d'un utilisateur |
| `GET /getNearbyAttractions?userName=<user>` | 5 attractions les plus proches, avec distance et points de récompense |
| `GET /getRewards?userName=<user>` | Récompenses accumulées par un utilisateur |
| `GET /getTripDeals?userName=<user>` | Forfaits voyages personnalisés |

Pour tester rapidement, des utilisateurs internes sont générés au démarrage : `internalUser0`, `internalUser1`, etc.

---

## Tests

```bash
cd TourGuide
mvn test
```

Suites présentes :

- `TestTourGuideService` — services principaux (3 tests)
- `TestRewardsService` — calcul des récompenses, dont `nearAllAttractions` (2 tests)
- `TestPerformance` — tests de charge à 100 000 utilisateurs (2 tests)
- `TourguideApplicationTests` — vérification du contexte Spring (1 test)

---

## Performances

Mesures obtenues sur ma machine de test (AMD Ryzen 9 7900X, 32 Go de RAM, JDK 23) :

| Test | Objectif OC | Mesure |
|---|---|---|
| `highVolumeTrackLocation` (100 000 users) | ≤ 15 min | 3 min 23 s |
| `highVolumeGetRewards` (100 000 users) | ≤ 20 min | 1 min 45 s |

La démarche complète (profiling VisualVM, choix de la parallélisation par `CompletableFuture`, tuning empirique des pools, sécurisation des collections concurrentes) est détaillée dans la documentation technique livrée avec le projet.

---

## Pipeline d'intégration continue

Workflow GitHub Actions (`.github/workflows/build.yml`) déclenché sur :
- push vers `dev` ou `master`,
- pull request vers `master`,
- déclenchement manuel via `workflow_dispatch`.

Étapes : checkout → setup Java 17 → installation des JARs locaux dans le dépôt Maven du runner → `mvn test` → `mvn package -DskipTests` → upload du JAR comme artefact téléchargeable (rétention 30 jours).

---

## Organisation du dépôt

Le projet Maven se trouve dans le sous-dossier `TourGuide/` (sources, tests et `pom.xml`). Les trois JARs locaux non publiés sur Maven Central sont conservés dans `TourGuide/libs/`. La configuration du pipeline est dans `.github/workflows/build.yml`.

---

## Auteur

Antoine Filho — Parcours Développeur d'application Java, OpenClassrooms.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import java.util.List;

import com.openclassrooms.tourguide.dto.AttractionsDto;
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;
Expand All @@ -32,19 +32,10 @@ public VisitedLocation getLocation(@RequestParam String userName) {
return tourGuideService.getUserLocation(getUser(userName));
}

// TODO: Change this method to no longer return a List of Attractions.
// Instead: Get the closest five tourist attractions to the user - no matter how far away they are.
// Return a new JSON object that contains:
// Name of Tourist attraction,
// Tourist attractions lat/long,
// The user's location lat/long,
// 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<Attraction> getNearbyAttractions(@RequestParam String userName) {
VisitedLocation visitedLocation = tourGuideService.getUserLocation(getUser(userName));
return tourGuideService.getNearByAttractions(visitedLocation);
@RequestMapping("/getNearbyAttractions")
public List<AttractionsDto> getNearbyAttractions(@RequestParam String userName) {
VisitedLocation visitedLocation = tourGuideService.getUserLocation(getUser(userName));
return tourGuideService.getNearByAttractions(visitedLocation);
}

@RequestMapping("/getRewards")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.openclassrooms.tourguide.dto;

public record AttractionsDto(
String attractionName,
double attractionLatitude,
double attractionLongitude,
double userLatitude,
double userLongitude,
double distanceInMiles,
int rewardPoints) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package com.openclassrooms.tourguide.service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

import gpsUtil.GpsUtil;
Expand All @@ -15,13 +21,15 @@
@Service
public class RewardsService {
private static final double STATUTE_MILES_PER_NAUTICAL_MILE = 1.15077945;
private static final int REWARDS_THREAD_POOL_SIZE = 500;

// 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 ExecutorService executorService = Executors.newFixedThreadPool(REWARDS_THREAD_POOL_SIZE);

public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) {
this.gpsUtil = gpsUtil;
Expand All @@ -37,19 +45,32 @@ public void setDefaultProximityBuffer() {
}

public void calculateRewards(User user) {
List<VisitedLocation> userLocations = user.getVisitedLocations();
// pour éviter une ConcurrentModificationException si le Tracker (thread
// d'arrière-plan) ajoute des locations pendant qu'on itère.
List<VisitedLocation> userLocations = new ArrayList<>(user.getVisitedLocations());
List<Attraction> 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)));
user.addUserReward(new UserReward(visitedLocation, attraction, getRewardPoints(attraction, user.getUserId())));
}
}
}
}
}
public CompletableFuture<Void> calculateRewardsAsync(User user) {
return CompletableFuture.runAsync(() -> calculateRewards(user), executorService);
}

/**
* Ferme proprement le pool de threads. À appeler en fin de tests.
*/
@PreDestroy
public void shutdownExecutor() {
executorService.shutdown();
}

public boolean isWithinAttractionProximity(Attraction attraction, Location location) {
return getDistance(attraction, location) > attractionProximityRange ? false : true;
Expand All @@ -58,9 +79,9 @@ public boolean isWithinAttractionProximity(Attraction attraction, Location locat
private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attraction) {
return getDistance(attraction, visitedLocation.location) > proximityBuffer ? false : true;
}
private int getRewardPoints(Attraction attraction, User user) {
return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId());

int getRewardPoints(Attraction attraction, UUID userId) {
return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, userId);
}

public double getDistance(Location loc1, Location loc2) {
Expand Down
Loading
Loading