diff --git a/metarParser-spi/pom.xml b/metarParser-spi/pom.xml index d8d97fe6..ca03eb1b 100644 --- a/metarParser-spi/pom.xml +++ b/metarParser-spi/pom.xml @@ -13,9 +13,9 @@ metarParser-spi - 0.96 - 1 - 1 + 0.94 + 0.75 + 0.62 diff --git a/metarParser-spi/src/main/java/io/github/mivek/provider/airport/impl/DefaultAirportProvider.java b/metarParser-spi/src/main/java/io/github/mivek/provider/airport/impl/DefaultAirportProvider.java index 1408f52d..11d6104f 100644 --- a/metarParser-spi/src/main/java/io/github/mivek/provider/airport/impl/DefaultAirportProvider.java +++ b/metarParser-spi/src/main/java/io/github/mivek/provider/airport/impl/DefaultAirportProvider.java @@ -20,69 +20,100 @@ * @author mivek */ public final class DefaultAirportProvider implements AirportProvider { - /** Path of airport file. */ - private final InputStream airportsFile = DefaultAirportProvider.class.getClassLoader().getResourceAsStream("data/airports.dat"); - /** Path of countries file. */ - private final InputStream countriesFile = DefaultAirportProvider.class.getClassLoader().getResourceAsStream("data/countries.dat"); - /** Map of countries. */ - private Map countries; - /** Map of airports. */ - private Map airports; + private static final String AIRPORTS_RESOURCE = "data/airports.dat"; + private static final String COUNTRIES_RESOURCE = "data/countries.dat"; + + private volatile Map countries; + private volatile Map airports; + + /** private lock to avoid exposing the monitor. */ + private final Object loadLock = new Object(); - /** - * Default constructor. - */ - public DefaultAirportProvider() { - initCountries(); - initAirports(); - } /** - * Initiate countries map. + * Ensure the airport and country data have been loaded. + * + *

This method is safe to call from multiple threads. It performs a double-checked + * locking pattern using {@code loadLock} to initialize the data only once. */ - private void initCountries() { - Objects.requireNonNull(countriesFile); - countries = new HashMap<>(); - try (CSVParser parser = CSVFormat.DEFAULT.parse(new InputStreamReader(countriesFile, StandardCharsets.UTF_8))) { - for (CSVRecord line : parser) { - Country country = new Country(); - country.setName(line.get(0)); - countries.put(country.getName(), country); + private void ensureLoaded() { + if (airports != null && countries != null) { + return; + } + synchronized (loadLock) { + if (airports != null && countries != null) { + return; } - } catch (IOException exception) { - throw new IllegalStateException(exception.getMessage()); + loadResources(); } } /** - * Initiate airports map. + * Load countries and airports from the bundled CSV resources into memory. + * + *

The method reads two CSV resource files (COUNTRIES_RESOURCE and AIRPORTS_RESOURCE), + * creates Country and Airport instances and populates internal maps. If a resource is not + * found or an I/O error occurs, this method throws an IllegalStateException wrapping the + * original cause. + * + * @throws IllegalStateException if a resource file cannot be found or an I/O error occurs + * while reading the CSV files */ - private void initAirports() { - Objects.requireNonNull(airportsFile); - airports = new HashMap<>(); - try (CSVParser parser = CSVFormat.DEFAULT.parse(new InputStreamReader(airportsFile, StandardCharsets.UTF_8))) { - for (CSVRecord line : parser) { - Airport airport = new Airport(); - airport.setName(line.get(1)); - airport.setCity(line.get(2)); - airport.setCountry(countries.get(line.get(3))); - airport.setIata(line.get(4)); - airport.setIcao(line.get(5)); - airport.setLatitude(Double.parseDouble(line.get(6))); - airport.setLongitude(Double.parseDouble(line.get(7))); - airport.setAltitude(Integer.parseInt(line.get(8))); - airport.setTimezone(line.get(9)); - airport.setDst(line.get(10)); - airports.put(airport.getIcao(), airport); + private void loadResources() { + Map localCountries = new HashMap<>(); + Map localAirports = new HashMap<>(); + + try (InputStream countriesStream = getClass().getClassLoader().getResourceAsStream(COUNTRIES_RESOURCE)) { + Objects.requireNonNull(countriesStream, COUNTRIES_RESOURCE + " not found"); + try (CSVParser parser = CSVFormat.DEFAULT.parse(new InputStreamReader(countriesStream, StandardCharsets.UTF_8))) { + for (CSVRecord line : parser) { + Country country = new Country(); + country.setName(line.get(0)); + localCountries.put(country.getName(), country); + } } - } catch (IOException exception) { - throw new IllegalStateException(exception.getMessage()); + } catch (IOException e) { + throw new IllegalStateException(e); } + + try (InputStream airportsStream = getClass().getClassLoader().getResourceAsStream(AIRPORTS_RESOURCE)) { + Objects.requireNonNull(airportsStream, AIRPORTS_RESOURCE + " not found"); + try (CSVParser parser = CSVFormat.DEFAULT.parse(new InputStreamReader(airportsStream, StandardCharsets.UTF_8))) { + for (CSVRecord line : parser) { + Airport airport = new Airport(); + airport.setName(line.get(1)); + airport.setCity(line.get(2)); + airport.setCountry(localCountries.get(line.get(3))); + airport.setIata(line.get(4)); + airport.setIcao(line.get(5)); + airport.setLatitude(Double.parseDouble(line.get(6))); + airport.setLongitude(Double.parseDouble(line.get(7))); + airport.setAltitude(Integer.parseInt(line.get(8))); + airport.setTimezone(line.get(9)); + airport.setDst(line.get(10)); + localAirports.put(airport.getIcao(), airport); + } + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + + this.countries = localCountries; + this.airports = localAirports; } + /** + * Returns the map of loaded airports keyed by ICAO code. + * + *

When this method is called the first time, it triggers loading of the underlying + * country and airport resources. Subsequent calls return the cached map. The returned + * map is the internal map instance (not a defensive copy). + * + * @return the map of ICAO -> Airport + */ @Override public Map getAirports() { + ensureLoaded(); return airports; } } -