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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.ResourceBundle;

/**
* Messages class for internationalization.
* Messages class for internationalization. Thread-safe via ThreadLocal.
*
* @author mivek
*/
Expand All @@ -14,15 +14,14 @@ public final class Messages {
private static final Messages INSTANCE = new Messages();
/** Name of the bundle. */
private static final String BUNDLE_NAME = "internationalization.messages";
/** Bundle variable. */
private ResourceBundle fResourceBundle;
/** Per-thread bundle holder — thread-safe, no global Locale.setDefault(). */
private final ThreadLocal<ResourceBundle> bundleHolder =
ThreadLocal.withInitial(() -> ResourceBundle.getBundle(BUNDLE_NAME));

/**
* Private constructor.
*/
private Messages() {
fResourceBundle = ResourceBundle.getBundle(BUNDLE_NAME);
}
private Messages() {}

/**
* @return the Messages instance.
Expand All @@ -32,22 +31,30 @@ public static Messages getInstance() {
}

/**
* Sets the locale of the bundle.
* Sets the locale of the bundle for the current thread.
*
* @param locale the locale to set.
*/
public void setLocale(final Locale locale) {
Locale.setDefault(locale);
ResourceBundle.clearCache();
fResourceBundle = ResourceBundle.getBundle(BUNDLE_NAME, locale);
bundleHolder.set(ResourceBundle.getBundle(BUNDLE_NAME, locale));
}

/**
* Clears the locale for the current thread, resetting it to the JVM default.
*
* <p>Must be called in thread-pool environments (e.g., servlets, Spring)
* after each request to prevent locale leakage between tasks on the same thread.
*/
public void clearLocale() {
bundleHolder.remove();
}

/**
* @param message the string to get
* @return the translation of message
*/
public String getString(final String message) {
return fResourceBundle.getString(message);
return bundleHolder.get().getString(message);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public final class Converter {
**/
private static final Double SM_TO_KM = 1.609344;

/** Pattern to parse a visibility string composed of a numeric value and a unit. */
private static final Pattern VISIBILITY_PATTERN = Pattern.compile("(\\d+)([a-z,A-Z]+)");

/**
* Private constructor.
*/
Expand Down Expand Up @@ -125,7 +128,7 @@ public static float convertTemperature(final String sign, final String temperatu
* @return The visibility in km as a double
*/
public static Double convertVisibilityToKM(final String visibility) {
final Matcher matcher = Pattern.compile("(\\d+)([a-z,A-Z]+)").matcher(visibility.replace(">", ""));
final Matcher matcher = VISIBILITY_PATTERN.matcher(visibility.replace(">", ""));
if (!matcher.find()) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Converter.NNE=Nord Nord Est
Converter.NNW=Nord Nord Ouest
Converter.NSC=Aucun changement significatif
Converter.NW=Nord Ouest
Converter.S=Est
Converter.S=Sud
Converter.SE=Sud Est
Converter.SSE=Sud Sud Est
Converter.SSW=Sud Sud Ouest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
package io.github.mivek.internationalization;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MessagesTest {

Expand All @@ -19,4 +30,36 @@ void testSetLocale() {
assertEquals("few", Messages.getInstance().getString("CloudQuantity.FEW"));
assertEquals("ceiling varying between 5 and 15 feet", Messages.getInstance().getString("Remark.Ceiling.Height", 5, 15));
}

@Test
void testClearLocale() {
Messages.getInstance().setLocale(Locale.FRENCH);
assertEquals("peu", Messages.getInstance().getString("CloudQuantity.FEW"));
Messages.getInstance().clearLocale();
// After clearing, the JVM default locale is used; the key must still be resolvable.
assertDoesNotThrow(() -> Messages.getInstance().getString("CloudQuantity.FEW"));
}

@ParameterizedTest
@ValueSource(strings = {"messages_de", "messages_es", "messages_fr", "messages_it",
"messages_pl_PL", "messages_ru_RU", "messages_tr_TR", "messages_zh_CN"})
@Disabled("Requires all locale bundles to be complete and up-to-date with the base bundle")
void testLocaleContainsAllBaseKeys(final String bundleName) throws IOException {
Properties base = loadProperties("internationalization/messages.properties");
Properties locale = loadProperties("internationalization/" + bundleName + ".properties");
Set<Object> baseKeys = base.keySet();
for (Object key : baseKeys) {
assertTrue(locale.containsKey(key),
"Locale bundle '" + bundleName + "' is missing key: " + key);
}
}

private Properties loadProperties(final String resourcePath) throws IOException {
Properties props = new Properties();
try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath);
InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
props.load(reader);
}
return props;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.mivek.enums;

import io.github.mivek.internationalization.Messages;
import java.util.regex.Pattern;

/**
* Enumeration for descriptive. The first attribute is the code used in the
Expand Down Expand Up @@ -28,13 +29,16 @@ public enum Descriptive {

/** The descriptive's shortcut. */
private final String shortcut;
/** Pre-compiled pattern used to detect this descriptive in a weather token. */
private final Pattern pattern;

/**
* Constructor.
* @param shortcut the shortcut of the descriptive.
*/
Descriptive(final String shortcut) {
this.shortcut = shortcut;
this.pattern = Pattern.compile("(" + shortcut + ")");
}

/**
Expand All @@ -44,6 +48,15 @@ public String getShortcut() {
return this.shortcut;
}

/**
* Returns the pre-compiled pattern used to match this descriptive in a weather token.
*
* @return the compiled {@link Pattern}.
*/
public Pattern getPattern() {
return pattern;
}

@Override
public String toString() {
return Messages.getInstance().getString("Descriptive." + shortcut);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.mivek.enums;

import io.github.mivek.internationalization.Messages;
import java.util.regex.Pattern;

/**
* Enumeration for phenomenon.
Expand Down Expand Up @@ -57,6 +58,8 @@ public enum Phenomenon {

/** Shortcut of the phenomenon. */
private final String shortcut;
/** Pre-compiled pattern used to match this phenomenon at the start of a weather token. */
private final Pattern pattern;

/**
* Constructor.
Expand All @@ -65,6 +68,7 @@ public enum Phenomenon {
*/
Phenomenon(final String shortcut) {
this.shortcut = shortcut;
this.pattern = Pattern.compile("^" + shortcut);
}

@Override
Expand All @@ -80,4 +84,13 @@ public String toString() {
public String getShortcut() {
return shortcut;
}

/**
* Returns the pre-compiled pattern used to match this phenomenon at the start of a weather token.
*
* @return the compiled {@link Pattern}.
*/
public Pattern getPattern() {
return pattern;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ WeatherCondition parseWeatherCondition(final String weatherPart) {
weatherPartCopy = weatherPartCopy.substring(i.getShortcut().length());
}
for (Descriptive des : Descriptive.values()) {
if (Regex.findString(Pattern.compile("(" + des.getShortcut() + ")"), weatherPart) != null) {
if (Regex.findString(des.getPattern(), weatherPart) != null) {
wc.setDescriptive(des);
weatherPartCopy = weatherPartCopy.substring(des.getShortcut().length());
break;
Expand All @@ -74,7 +74,7 @@ WeatherCondition parseWeatherCondition(final String weatherPart) {
while (!weatherPartCopy.isEmpty() && !weatherPartCopy.equals(previousToken)) {
previousToken = weatherPartCopy;
for (Phenomenon phenom: Phenomenon.values()) {
if (Regex.find(Pattern.compile("^" + phenom.getShortcut()), weatherPartCopy)) {
if (Regex.find(phenom.getPattern(), weatherPartCopy)) {
wc.addPhenomenon(phenom);
weatherPartCopy = weatherPartCopy.substring(phenom.getShortcut().length());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
Expand All @@ -23,28 +24,31 @@ public final class DefaultAirportProvider implements AirportProvider {
private static final String AIRPORTS_RESOURCE = "data/airports.dat";
private static final String COUNTRIES_RESOURCE = "data/countries.dat";

private volatile Map<String, Country> countries;
private volatile Map<String, Airport> airports;
/** Whether the data has been loaded. Set to true only after airports map is fully populated. */
private volatile boolean initialized;
/** Map of airports keyed by ICAO code. */
private Map<String, Airport> airports;

/** private lock to avoid exposing the monitor. */
/** Private lock to avoid exposing the monitor. */
private final Object loadLock = new Object();


/**
* Ensure the airport and country data have been loaded.
*
* <p>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.
* <p>This method is safe to call from multiple threads. It uses a double-checked
* locking pattern on a single {@code volatile boolean} flag so that a thread
* never observes a partially-initialized state.
*/
private void ensureLoaded() {
if (airports != null && countries != null) {
if (initialized) {
return;
}
synchronized (loadLock) {
if (airports != null && countries != null) {
if (initialized) {
return;
}
loadResources();
initialized = true;
}
}

Expand Down Expand Up @@ -98,22 +102,20 @@ private void loadResources() {
throw new IllegalStateException(e);
}

this.countries = localCountries;
this.airports = localAirports;
}

/**
* Returns the map of loaded airports keyed by ICAO code.
* Returns an unmodifiable view of the loaded airports keyed by ICAO code.
*
* <p>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).
* country and airport resources. Subsequent calls return the cached map.
*
* @return the map of ICAO -> Airport
* @return an unmodifiable map of ICAO -> Airport
*/
@Override
public Map<String, Airport> getAirports() {
ensureLoaded();
return airports;
return Collections.unmodifiableMap(airports);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.csv.CSVFormat;
Expand Down Expand Up @@ -113,7 +114,7 @@ public void buildAirport() throws URISyntaxException, IOException, InterruptedEx

@Override
public Map<String, Airport> getAirports() {
return airports;
return Collections.unmodifiableMap(airports);
}
}

Loading