diff --git a/pom.xml b/pom.xml index 924eae8..a3c38c8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ net.curre.jjeopardy jjeopardy jar - 0.2.0 + 0.3.0 JJeopardy @@ -19,8 +19,8 @@ Copyright © 2024 Yevgeny Nyden - 1.8 - 1.8 + 1.9 + 1.9 UTF-8 UTF-8 @@ -94,10 +94,6 @@ org.apache.maven.plugins maven-compiler-plugin 2.5.1 - - 1.8 - 1.8 - maven-resources-plugin diff --git a/src/main/java/net/curre/jjeopardy/App.java b/src/main/java/net/curre/jjeopardy/App.java index 1955996..5214e3c 100755 --- a/src/main/java/net/curre/jjeopardy/App.java +++ b/src/main/java/net/curre/jjeopardy/App.java @@ -22,10 +22,17 @@ import net.curre.jjeopardy.service.Registry; import net.curre.jjeopardy.service.SettingsService; import net.curre.jjeopardy.ui.landing.LandingUi; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.FileAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.layout.PatternLayout; import javax.swing.SwingUtilities; +import java.io.File; /** * The driver to run the JJeopardy application. @@ -36,11 +43,17 @@ public class App { /** Private class logger. */ private static final Logger logger = LogManager.getLogger(App.class.getName()); + /** Name of the log file (will be created in the settings directory). */ + private static final String LOG_FILENAME = "jjeopardy.log"; + /** * Main method to run the JJeopardy application. * @param args Argument array. */ public static void main(String[] args) { + + // Add file appender to log4j configuration. + updateLogConfiguration(); logger.info("Starting application..."); // Initialize the main service registry with a default (non-test) one. @@ -62,4 +75,35 @@ public static void main(String[] args) { registry.setLandingUi(landingUi); }); } + + /** + * Adds a file appender to log4j configuration. It has to be done programmatically + * (vs via log4j2.xml) because the log filepath needs to be determined at run time. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void updateLogConfiguration() { + final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + final Configuration config = loggerContext.getConfiguration(); + final Layout layout = PatternLayout.newBuilder() + .withPattern("%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n") + .withConfiguration(config).build(); + String logFilePath = SettingsService.getVerifiedSettingsDirectoryPath() + File.separatorChar + LOG_FILENAME; + System.out.println("logFilePath = " + logFilePath); + FileAppender.Builder builder = FileAppender.newBuilder(); + builder.withFileName(logFilePath) + .withAppend(false) + .withLocking(false) + .setIgnoreExceptions(true); + builder.setName("JJeopardyLogFile"); + builder.setBufferedIo(true); + builder.setBufferSize(4000); + builder.setLayout(layout); + builder.setImmediateFlush(true); + builder.setConfiguration(config); + FileAppender appender = builder.build(); + appender.start(); + + config.getRootLogger().addAppender(appender, Level.INFO, null); + loggerContext.updateLoggers(); + } } diff --git a/src/main/java/net/curre/jjeopardy/games/DefaultGames.java b/src/main/java/net/curre/jjeopardy/games/DefaultGames.java index 604c0e6..c2e1133 100644 --- a/src/main/java/net/curre/jjeopardy/games/DefaultGames.java +++ b/src/main/java/net/curre/jjeopardy/games/DefaultGames.java @@ -16,40 +16,146 @@ package net.curre.jjeopardy.games; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * Serves as an anchor to the default games resource folder and a utility - * to load the default games. + * to copy default games (packaged with the game) to the games library folder. * * @author Yevgeny Nyden */ public class DefaultGames { + /** Private class logger. */ + private static final Logger logger = LogManager.getLogger(DefaultGames.class.getName()); + /** - * Gets the default game bundle from the default games folder. - * @return a list of File objects representing default game bundles - * @throws URISyntaxException on loading error + * Copies prepackaged games from the resource directory (or a jar) to the library + * games folder. Only game bundles (not single game files) are supported. + * @param libGamesDir an absolute path to the game library folder (under game settings) + * @throws URISyntaxException on URI syntax errors + * @throws IOException on input/output errors */ - public static List getDefaultGameBundles() throws URISyntaxException { - final List bundles = new ArrayList<>(); + public static void copyDefaultLibraryGames(Path libGamesDir) throws URISyntaxException, IOException { // Obtaining the list of game bundles (directories) in the games directory. - URL resource = DefaultGames.class.getResource(""); - for (File gameBundle : Objects.requireNonNull(Paths.get(resource.toURI()).toFile().listFiles())) { - if (StringUtils.startsWith(gameBundle.getName(), DefaultGames.class.getSimpleName())) { - // Skipping the class file. + URL gameDirResource = DefaultGames.class.getResource(""); + + if (gameDirResource == null) { + logger.error("Unable to find prepackaged library games resource"); + } else if (gameDirResource.getPath().startsWith("file:")) { + // Running in prod mode (reading from jar). + String rootPath = StringUtils.split(gameDirResource.getPath(), "!")[1].substring(1); + String jarPath = StringUtils.substringBetween(gameDirResource.getPath(), "file:", "!"); + copyFromJarFile(jarPath, rootPath, libGamesDir); + } else { + // Running in dev mode (reading from files). + File sourceDir = Paths.get(gameDirResource.toURI()).toFile(); + copyFromFileSystem(sourceDir, libGamesDir); + } + } + + /** + * Copies game bundles from the jar to the specified library location. + * @param jarPath absolute path to the game jar file + * @param gamesResourcePath resource path to the games data + * @param libGamesDir absolute path to the games library folder + * @throws IOException on input/output errors + */ + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void copyFromJarFile(String jarPath, String gamesResourcePath, Path libGamesDir) throws IOException { + logger.info("Opening the app jar: " + jarPath); + FileInputStream fileInputStream = new FileInputStream(jarPath); + BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); + ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream); + + // Go over the zip file entries and copy the ones that match our pattern. + int rootPathCharPivot = gamesResourcePath.length(); + ZipEntry zipEntry = null; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (zipEntry.getName().startsWith(gamesResourcePath)) { + logger.debug(":::: resource " + zipEntry.getName().substring(rootPathCharPivot)); + String gameResource = zipEntry.getName().substring(rootPathCharPivot); + String[] parts = gameResource.split("/"); + // Only pick paths that have both 'bundle' and 'file' (game data file). + if (parts.length == 2) { + String bundleName = parts[0]; + String fileName = parts[1]; + File destDir = new File(libGamesDir.toString() + File.separatorChar + bundleName); + if (!destDir.exists()) { + destDir.mkdir(); + } + String destFileName = destDir.toString() + File.separatorChar + fileName; + extractZipEntry(zipInputStream, destFileName); + } else if (parts.length > 2) { + // TODO: add support for images ('bundle' / 'games' / 'file'). + logger.warn("Library games with directories are not supported yet: " + gameResource); + } + } + } + zipInputStream.close(); + } + + /** + * Extracts a single zip file entry/file - copies it to the destination filepath. + * @param zipInputStream zip input stream to read + * @param destFilePath absolute path to the filename where zip entry is copied to + * @throws IOException on input/output errors + */ + private static void extractZipEntry(ZipInputStream zipInputStream, String destFilePath) throws IOException { + OutputStream outStream = Files.newOutputStream(Paths.get(destFilePath)); + byte[] buffer = new byte[9000]; + int length; + while ((length = zipInputStream.read(buffer)) != -1) { + outStream.write(buffer, 0, length); + } + outStream.close(); + } + + /** + * Copies prepackaged games from the filesystem to the games library folder. + * @param sourceDir source directory where library game bundles are located + * @param libGamesDir an absolute path to the game library folder (under game settings) + * where games are copied to + */ + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void copyFromFileSystem(File sourceDir, Path libGamesDir) throws IOException { + // List the game bundles in the resources games directory. + final List gameBundles = new ArrayList<>(); + for (File gameBundle : Objects.requireNonNull(sourceDir.listFiles())) { + if (gameBundle.isFile()) { + // Skipping any non-directory item. continue; } - bundles.add(gameBundle); + gameBundles.add(gameBundle); + } + + // Copy resource game directories and their content to the library games directory. + for (File originalBundle : gameBundles) { + File destDir = new File(libGamesDir.toString() + File.separatorChar + originalBundle.getName()); + destDir.mkdir(); + for (File file : Objects.requireNonNull(originalBundle.listFiles())) { + File destFile = new File(destDir.toString() + File.separatorChar + file.getName()); + FileUtils.copyFile(file, destFile); + } } - return bundles; } } diff --git a/src/main/java/net/curre/jjeopardy/service/GameDataService.java b/src/main/java/net/curre/jjeopardy/service/GameDataService.java index f968336..b6b9fdc 100755 --- a/src/main/java/net/curre/jjeopardy/service/GameDataService.java +++ b/src/main/java/net/curre/jjeopardy/service/GameDataService.java @@ -206,23 +206,18 @@ public boolean isGameReady() { public static void copyDefaultGamesToLibraryIfNeeded() { Path gamesDir = Paths.get(getGameLibraryDirectoryPath()); if (Files.exists(gamesDir)) { + logger.info("It appears that the library games were copied already to: " + gamesDir); // If the games directory exists, assume the files have been copied there already. return; } + logger.info("Copying the library games to: " + gamesDir); try { gamesDir.toFile().mkdir(); - List gameBundles = DefaultGames.getDefaultGameBundles(); - for (File originalBundle : gameBundles) { - File destDir = new File(gamesDir.toString() + File.separatorChar + originalBundle.getName()); - destDir.mkdir(); - for (File file : Objects.requireNonNull(originalBundle.listFiles())) { - File destFile = new File(destDir.toString() + File.separatorChar + file.getName()); - FileUtils.copyFile(file, destFile); - } - } + DefaultGames.copyDefaultLibraryGames(gamesDir); } catch (Exception e) { - logger.log(Level.WARN, "Unable to copy default game files", e); + logger.log(Level.ERROR, "Unable to copy default game files", e); } + logger.info("Finished copying the library games"); } /** diff --git a/src/main/java/net/curre/jjeopardy/service/LocaleService.java b/src/main/java/net/curre/jjeopardy/service/LocaleService.java index b73a0f9..6f16cb4 100644 --- a/src/main/java/net/curre/jjeopardy/service/LocaleService.java +++ b/src/main/java/net/curre/jjeopardy/service/LocaleService.java @@ -16,7 +16,6 @@ package net.curre.jjeopardy.service; -import net.curre.jjeopardy.App; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -41,6 +40,9 @@ public class LocaleService { /** Private class logger. */ private static final Logger logger = LogManager.getLogger(LocaleService.class.getName()); + /** Currently selected locale for the JJeopardy app. */ + private static Locale currentLocale = DEFAULT_LOCALE; + /** List of available locales in the application. */ private final List availableLocales; @@ -55,7 +57,6 @@ protected LocaleService() { this.availableLocales.add(new Locale(localeParts[0], localeParts[1])); } } - Locale.setDefault(DEFAULT_LOCALE); } /** @@ -66,6 +67,14 @@ public List getAvailableLocales() { return this.availableLocales; } + /** + * Gets the currently selected locale for the JJeopardy app. + * @return locale to use in the app + */ + public static Locale getCurrentLocale() { + return currentLocale; + } + /** * Gets the string resource for the given key. If any keyArgs * are passed, they will replace the "{0}", "{1}", etc. string @@ -77,10 +86,9 @@ public List getAvailableLocales() { * @return string resource for the given key */ public static String getString(String key, String... keyArgs) { - Locale locale = Locale.getDefault(); String bundleName = "messages"; - if (!locale.equals(DEFAULT_LOCALE)) { - bundleName += "_" + locale; + if (!currentLocale.equals(DEFAULT_LOCALE)) { + bundleName += "_" + currentLocale; } String message = ResourceBundle.getBundle(bundleName).getString(key); if (keyArgs != null) { @@ -99,15 +107,14 @@ public static String getString(String key, String... keyArgs) { } /** - * Setter for the current locale. - * @param localeId Identifier for the locale (Locale.toString()) - * @param showDialog true if show restart info dialog on change + * Setter for the current locale used by the application. + * @param localeId identifier for the locale (Locale.toString()) + * @param showDialog true if locale changed; false if otherwise */ public synchronized void setCurrentLocale(String localeId, boolean showDialog) { try { - Locale prevLocale = Locale.getDefault(); - Locale locale = findLocaleById(localeId); - Locale.setDefault(locale); + Locale prevLocale = LocaleService.getCurrentLocale(); + currentLocale = findLocaleById(localeId); if (!localeId.equals(prevLocale.toString()) && showDialog) { AppRegistry.getInstance().getUiService().showRestartGameDialog(); @@ -134,4 +141,12 @@ public Locale findLocaleById(String localeId) { logger.warn("Locale \"" + localeId + "\" is not found!"); return DEFAULT_LOCALE; } + + /** + * Sets the current locale used by the application. + * @param locale locale to set as the current locale + */ + protected static void setCurrentLocale(Locale locale) { + currentLocale = locale; + } } diff --git a/src/main/java/net/curre/jjeopardy/service/SettingsService.java b/src/main/java/net/curre/jjeopardy/service/SettingsService.java index 824680d..a63e700 100755 --- a/src/main/java/net/curre/jjeopardy/service/SettingsService.java +++ b/src/main/java/net/curre/jjeopardy/service/SettingsService.java @@ -16,7 +16,6 @@ package net.curre.jjeopardy.service; -import net.curre.jjeopardy.App; import net.curre.jjeopardy.bean.Settings; import net.curre.jjeopardy.util.JjDefaults; import net.curre.jjeopardy.util.Utilities; @@ -24,8 +23,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.io.*; -import java.util.Locale; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import static net.curre.jjeopardy.service.LafService.DEFAULT_LAF_THEME_ID; @@ -41,7 +43,7 @@ * Settings file is stored in a platform specific directory, *
    *
  • on Mac, it's - 'UserHome' / Library / Application Support / JJeopardy /
  • - *
  • on Windows, it's - 'UserHome' / AppData / Local / Temp / JJeopardy /
  • + *
  • on Windows, it's - 'UserHome' / AppData / Local / JJeopardy /
  • *
  • on others, it's - 'UserHome' / temp / JJeopardy /
  • *
* @@ -148,8 +150,7 @@ public static String getVerifiedSettingsDirectoryPath() { break; case WINDOWS: path.append(File.separatorChar).append("AppData"). - append(File.separatorChar).append("Local"). - append(File.separatorChar).append("Temp"); + append(File.separatorChar).append("Local"); break; default: path.append(File.separatorChar).append("temp"); @@ -209,9 +210,7 @@ private static void verifySettings(Settings settings) { settings.setLafThemeId(DEFAULT_LAF_THEME_ID); } if (settings.getLocaleId() == null) { - LocaleService localeService = AppRegistry.getInstance().getLocaleService(); - Locale locale = localeService.findLocaleById(Locale.getDefault().toString()); - settings.setLocaleId(locale.toString()); + settings.setLocaleId(LocaleService.getCurrentLocale().toString()); } if (settings.getLastCurrentDirectory() == null) { settings.setLastCurrentDirectory(System.getProperty("user.home")); diff --git a/src/main/java/net/curre/jjeopardy/ui/landing/LandingUiMenu.java b/src/main/java/net/curre/jjeopardy/ui/landing/LandingUiMenu.java index 3ef2c11..921d955 100644 --- a/src/main/java/net/curre/jjeopardy/ui/landing/LandingUiMenu.java +++ b/src/main/java/net/curre/jjeopardy/ui/landing/LandingUiMenu.java @@ -173,7 +173,7 @@ private JMenu createLocaleMenu() { LocaleService localeService = AppRegistry.getInstance().getLocaleService(); for (Locale locale : localeService.getAvailableLocales()) { JRadioButtonMenuItem localeItem = new JRadioButtonMenuItem(locale.getDisplayName()); - if (locale.equals(Locale.getDefault())) { + if (locale.equals(LocaleService.getCurrentLocale())) { localeItem.setSelected(true); } localesGroup.add(localeItem); diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index ab8a49f..c65d606 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -2,12 +2,12 @@ - + - + diff --git a/src/test/java/net/curre/jjeopardy/TestRunner.java b/src/test/java/net/curre/jjeopardy/TestRunner.java index d1f5e44..ad8f755 100644 --- a/src/test/java/net/curre/jjeopardy/TestRunner.java +++ b/src/test/java/net/curre/jjeopardy/TestRunner.java @@ -23,13 +23,18 @@ import java.awt.Font; import java.awt.GraphicsEnvironment; import java.awt.event.ActionListener; +import java.io.BufferedInputStream; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * Just a handy runner to execute bits of test code "by hand". @@ -41,19 +46,74 @@ public class TestRunner { public static void main(final String[] args) { - testRegExStuff(); + try { + testUnzipping(); + +// testRegExStuff(); // testProgressDialog(); /* - try { testCreatingPropertiesFile(); - } catch (IOException e) { - throw new RuntimeException(e); - } */ // printAvailableFonts(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void testUnzipping() throws Exception { + String fileToBeExtracted1 = "net/curre/jjeopardy/games/halloween-fun.jj/halloween-fun.xml"; + String out1 = "q_halloween-fun.xml"; + String fileToBeExtracted2 = "net/curre/jjeopardy/games/the-big-great-wolrd.jj/the-big-great-wolrd.xml"; + String out2 = "q_the-big-great-wolrd.xml"; + String zipPackage = "target/jjeopardy-0.3.0.jar"; + FileInputStream fileInputStream = new FileInputStream(zipPackage); + BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream ); + ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream); + ZipEntry ze = null; + while ((ze = zipInputStream.getNextEntry()) != null) { + if (ze.getName().equals(fileToBeExtracted1)) { + extractOneFile(zipInputStream, out1); + } else if (ze.getName().equals(fileToBeExtracted2)) { + extractOneFile(zipInputStream, out2); + } + } + zipInputStream.close(); + } + + private static void extractOneFile(ZipInputStream zin, String outName) throws IOException { + OutputStream out = new FileOutputStream(outName); + byte[] buffer = new byte[9000]; + int len; + while ((len = zin.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + out.close(); + } + + public static void testUnzipping2() throws Exception { + String fileToBeExtracted = "net/curre/jjeopardy/games/halloween-fun.jj/halloween-fun.xml"; + String zipPackage = "target/jjeopardy-0.3.0.jar"; + OutputStream out = new FileOutputStream("fileToBeExtracted.xml"); + FileInputStream fileInputStream = new FileInputStream(zipPackage); + BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream ); + ZipInputStream zin = new ZipInputStream(bufferedInputStream); + ZipEntry ze = null; + while ((ze = zin.getNextEntry()) != null) { + if (ze.getName().equals(fileToBeExtracted)) { + byte[] buffer = new byte[9000]; + int len; + while ((len = zin.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + out.close(); + break; + } + } + zin.close(); } public static void testRegExStuff() { diff --git a/src/test/java/net/curre/jjeopardy/service/FileParsingResultTest.java b/src/test/java/net/curre/jjeopardy/service/FileParsingResultTest.java index b02ee30..9acd4cc 100644 --- a/src/test/java/net/curre/jjeopardy/service/FileParsingResultTest.java +++ b/src/test/java/net/curre/jjeopardy/service/FileParsingResultTest.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Locale; -import static net.curre.jjeopardy.service.LocaleService.DEFAULT_LOCALE; import static org.junit.Assert.*; /** @@ -45,7 +44,6 @@ public class FileParsingResultTest { @Before public void init() { this.testResult = new FileParsingResult(TEST_FILE_NAME); - Locale.setDefault(DEFAULT_LOCALE); } /** Tests initialization of the default object state. */ @@ -78,7 +76,7 @@ public void testMessageEnums() { List locales = localeService.getAvailableLocales(); assertNotNull("List of locales is null", locales); for (Locale locale : locales) { - Locale.setDefault(locale); + LocaleService.setCurrentLocale(locale); assertResultMessagesSet(); } } @@ -120,7 +118,7 @@ public void testAddingErrorMessages() { * Asserts all Message enums are valid (have corresponding properties for the current locale). */ private static void assertResultMessagesSet() { - Locale locale = Locale.getDefault(); + Locale locale = LocaleService.getCurrentLocale(); for (FileParsingResult.Message message : FileParsingResult.Message.values()) { String propName = message.getPropertyName(); assertFalse("Property name for " + message + " should not be blank", StringUtils.isBlank(propName)); diff --git a/src/test/java/net/curre/jjeopardy/service/LocaleServiceTest.java b/src/test/java/net/curre/jjeopardy/service/LocaleServiceTest.java index 18265b2..6d9597e 100644 --- a/src/test/java/net/curre/jjeopardy/service/LocaleServiceTest.java +++ b/src/test/java/net/curre/jjeopardy/service/LocaleServiceTest.java @@ -38,20 +38,6 @@ public class LocaleServiceTest { public void init() { } - /** Tests initialization of default locale service state. */ - @Test - public void testDefaultLocaleService() { - LocaleService localeService = new LocaleService(); - List locales = localeService.getAvailableLocales(); - assertNotNull("List of locales is null", locales); - assertEquals("Wrong number of supported locales", 2, locales.size()); - assertEquals("Wrong first locale", LocaleService.DEFAULT_LOCALE, locales.get(0)); - - assertEquals("Wrong default locale", "en_US", LocaleService.DEFAULT_LOCALE.toString()); - Locale locale = Locale.getDefault(); - assertEquals("Wrong default locale", LocaleService.DEFAULT_LOCALE, locale); - } - /** Tests setCurrentLocale. */ @Test public void testSetCurrentLocale() { @@ -62,8 +48,7 @@ public void testSetCurrentLocale() { assertEquals("Wrong first locale", LocaleService.DEFAULT_LOCALE, locales.get(0)); localeService.setCurrentLocale(locales.get(1).toString(), false); - Locale locale = Locale.getDefault(); - assertEquals("Wrong locale", locales.get(1), locale); + assertEquals("Wrong locale", locales.get(1), LocaleService.getCurrentLocale()); } /** Tests findLocaleById. */