-
Notifications
You must be signed in to change notification settings - Fork 0
feat: load stations data at startup #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements functionality to load Velib bike-sharing station data from the Paris Open Data API at application startup. It introduces API response models (VelibApiResponse, VelibRecord, VelibFields), a repository interface for station persistence, and a service that fetches and synchronizes station data with the database.
Key Changes:
- Added VelibService to fetch station data from external API and persist/update stations in the database
- Created data models for parsing the Velib API JSON responses
- Modified database schema to support station record IDs and change ID types from integer to string
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| VelibService.java | Core service for fetching and saving Velib station data with startup event listener |
| Station.java | Updated entity model with new fields (recordId, stationCode) and removed address field |
| StationRepository.java | JPA repository interface with custom finder method for station lookup |
| VelibApiResponse.java | Response wrapper model for API data deserialization |
| VelibRecord.java | Model representing individual station records from API |
| VelibFields.java | Model for station field data including coordinates and metadata |
| WebClientConfig.java | Configuration for Spring WebClient to call Paris Open Data API |
| V2__Alter_table_station_to_add_recordId_column.sql | Migration adding recordId column to station table |
| V3__Alter_tables_users_and_station_to_change_id_type.sql | Migration changing ID types from integer to VARCHAR |
| V4__Delete_address_column_in_table_station.sql | Migration removing address column from station table |
| pom.xml | Added spring-boot-starter-webflux dependency for reactive HTTP client |
| flyway.conf | Flyway configuration with database connection details |
| .gitignore | Added flyway.conf to ignored files |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private String recordId; | ||
| private String name; | ||
| @JsonProperty("stationcode") | ||
| private String station_code; |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name station_code does not follow Java naming conventions. Field names should be in camelCase. It should be named stationCode to be consistent with the getter method getStation_code() and Java conventions.
| Station station = new Station( | ||
| fields.getName(), | ||
| record.getRecordId(), | ||
| fields.getStation_code(), | ||
| fields.getCity(), | ||
| latitude, | ||
| longitude, | ||
| description | ||
| ); | ||
|
|
||
| return station; |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary variable assignment. The station variable is only used once and can be inlined. Replace lines 50-60 with:
return new Station(
fields.getName(),
record.getRecordId(),
fields.getStation_code(),
fields.getCity(),
latitude,
longitude,
description
);| @Service | ||
| public class VelibService { | ||
| public final WebClient webClient; | ||
| private final StationRepository stationRepository; | ||
|
|
||
| public VelibService(WebClient webClient, StationRepository stationRepository) { | ||
| this.webClient = webClient; | ||
| this.stationRepository = stationRepository; | ||
| } | ||
|
|
||
| public void fetchAndSaveVelibData() { | ||
|
|
||
| String queryUri = "?dataset=velib-disponibilite-en-temps-reel&rows=50"; | ||
|
|
||
| VelibApiResponse apiResponse = webClient.get() | ||
| .uri(queryUri) | ||
| .retrieve() | ||
| .bodyToMono(VelibApiResponse.class) | ||
| .block(); | ||
|
|
||
| if (apiResponse == null || apiResponse.getRecords() == null || apiResponse.getRecords().isEmpty()) { | ||
| System.out.println("Aucune donnée Velib reçue."); | ||
| return; | ||
| } | ||
|
|
||
| apiResponse.getRecords().stream() | ||
| .map(record -> { | ||
| VelibFields fields = record.getFields(); | ||
|
|
||
| List<Double> coords = fields.getCoordonnees_geo(); | ||
| double latitude = (coords != null && coords.size() >= 1) ? coords.get(0) : 0.0; | ||
| double longitude = (coords != null && coords.size() >= 2) ? coords.get(1) : 0.0; | ||
|
|
||
| String description = fields.getName() + ". Dernière mise à jour: " + fields.getDuedate(); | ||
|
|
||
| Station station = new Station( | ||
| fields.getName(), | ||
| record.getRecordId(), | ||
| fields.getStation_code(), | ||
| fields.getCity(), | ||
| latitude, | ||
| longitude, | ||
| description | ||
| ); | ||
|
|
||
| return station; | ||
| }) | ||
| .forEach(station ->{ | ||
| Station existingStation = stationRepository.findByStationCode(station.getStationCode()); | ||
| if(existingStation == null){ | ||
| stationRepository.save(station); | ||
| } else { | ||
| existingStation.setName(station.getName()); | ||
| //AJOUTER TOUTES LES MAJS | ||
| stationRepository.save(existingStation); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| @EventListener(ApplicationReadyEvent.class) | ||
| public void runAfterStartup() { | ||
| System.out.println("🚀 Démarrage de l'application détecté. Lancement de la récupération des données Velib..."); | ||
|
|
||
| try { | ||
| fetchAndSaveVelibData(); | ||
| System.out.println("✅ Récupération et sauvegarde initiales terminées."); | ||
| } catch (Exception e) { | ||
| System.err.println("❌ Erreur critique lors de la récupération initiale des données Velib : " + e.getMessage()); | ||
| } | ||
| } | ||
| } No newline at end of file |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The VelibService class lacks test coverage. Consider adding tests for:
fetchAndSaveVelibData()with various API responses (success, empty, null)- Handling of new vs existing stations
- Coordinate parsing edge cases
- Error handling when the API is unavailable
Example test structure:
@ExtendWith(MockitoExtension.class)
class VelibServiceTest {
@Mock
private WebClient webClient;
@Mock
private StationRepository stationRepository;
@Test
void fetchAndSaveVelibData_shouldSaveNewStations() {
// test implementation
}
}| stationRepository.save(station); | ||
| } else { | ||
| existingStation.setName(station.getName()); | ||
| //AJOUTER TOUTES LES MAJS |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete update logic for existing stations. Currently only the name field is updated, but the station object has other fields that should also be updated (recordId, city, latitude, longitude, description). The incomplete update could lead to stale data. Either implement all updates as mentioned in the TODO comment, or document why only the name needs updating:
existingStation.setName(station.getName());
existingStation.setRecordId(station.getRecordId());
existingStation.setCity(station.getCity());
existingStation.setLatitude(station.getLatitude());
existingStation.setLongitude(station.getLongitude());
existingStation.setDescription(station.getDescription());
stationRepository.save(existingStation);| //AJOUTER TOUTES LES MAJS | |
| existingStation.setRecordId(station.getRecordId()); | |
| existingStation.setCity(station.getCity()); | |
| existingStation.setLatitude(station.getLatitude()); | |
| existingStation.setLongitude(station.getLongitude()); | |
| existingStation.setDescription(station.getDescription()); |
| public String getCity(){ | ||
| return this.city; | ||
| } | ||
| public void setCity(String city){} |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setCity method has an empty implementation and does not actually set the city field. This should be implemented as:
public void setCity(String city) {
this.city = city;
}| public void setCity(String city){} | |
| public void setCity(String city){ | |
| this.city = city; | |
| } |
| stationRepository.save(station); | ||
| } else { | ||
| existingStation.setName(station.getName()); | ||
| //AJOUTER TOUTES LES MAJS |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] French comment in code. Consider using English for code comments to maintain consistency and accessibility for international developers:
//ADD ALL UPDATES| //AJOUTER TOUTES LES MAJS | |
| //ADD ALL UPDATES |
| flyway.url=jdbc:postgresql://185.166.39.209:5432/vlib_db | ||
| flyway.user=vlibadmin | ||
| flyway.password=T5tUb8DTh2dtMaRymzMyRKwT5kg3xqamdZJjqkLr |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Database credentials (URL, username, and password) are exposed in this configuration file. Even though flyway.conf is being added to .gitignore, the file itself is being committed to the repository in this PR, which means the credentials are now in the Git history and publicly accessible. These credentials should be rotated immediately and the file should be removed from the commit history. Use environment variables or a secure secrets management system instead.
| flyway.url=jdbc:postgresql://185.166.39.209:5432/vlib_db | |
| flyway.user=vlibadmin | |
| flyway.password=T5tUb8DTh2dtMaRymzMyRKwT5kg3xqamdZJjqkLr | |
| # Database credentials have been removed for security. | |
| # Set the following environment variables before running Flyway: | |
| # FLYWAY_URL | |
| # FLYWAY_USER | |
| # FLYWAY_PASSWORD |
|
|
||
| private VelibFields fields; | ||
| @JsonProperty("recordid") | ||
| public String recordId; |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The recordId field is declared as public, which violates encapsulation. It should be private like the other fields in this class, since there are already getter and setter methods for it.
| public String recordId; | |
| private String recordId; |
| .block(); | ||
|
|
||
| if (apiResponse == null || apiResponse.getRecords() == null || apiResponse.getRecords().isEmpty()) { | ||
| System.out.println("Aucune donnée Velib reçue."); |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using System.out.println and System.err.println for logging is not recommended in production applications. Consider using a proper logging framework like SLF4J with Logback (already included in Spring Boot). Replace with:
private static final Logger logger = LoggerFactory.getLogger(VelibService.class);
logger.info("Aucune donnée Velib reçue.");| double latitude = (coords != null && coords.size() >= 1) ? coords.get(0) : 0.0; | ||
| double longitude = (coords != null && coords.size() >= 2) ? coords.get(1) : 0.0; | ||
|
|
||
| String description = fields.getName() + ". Dernière mise à jour: " + fields.getDuedate(); |
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential NullPointerException: If fields.getDuedate() returns null (i.e., duedate is null), calling toString() on it will throw a NullPointerException. Add a null check:
String description = fields.getName() + ". Dernière mise à jour: " +
(fields.getDuedate() != null ? fields.getDuedate() : "inconnue");| String description = fields.getName() + ". Dernière mise à jour: " + fields.getDuedate(); | |
| String description = fields.getName() + ". Dernière mise à jour: " + (fields.getDuedate() != null ? fields.getDuedate() : "inconnue"); |
| @Bean | ||
| public WebClient webClient(){ | ||
| return WebClient.builder() | ||
| .baseUrl("https://opendata.paris.fr/api/records/1.0/search/") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass the URL through an environment variable
Description
Create classes :
Create Interface StationRepository
Create the VelibService class for parsing the Api call.
Create new migrations files to modify the table station
Related Issue
Closes #10
Type of Change
How Has This Been Tested?
No test yet.
Checklist
Comment