From 640c1039cfd6718ca03e25947f3e24e24d070c88 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:54:06 +0300 Subject: [PATCH] Add simulator auto bundle tests --- .../com/codename1/impl/javase/JavaSEPort.java | 288 +++++++++++++++++- .../javase/AutoLocalizationBundleTest.java | 98 ++++++ 2 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 3ac51157f5..68a86ccf07 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -188,12 +188,15 @@ public class JavaSEPort extends CodenameOneImplementation { private static final int ICON_SIZE=24; + private static final String PREF_AUTO_UPDATE_DEFAULT_BUNDLE = "cn1.autoDefaultResourceBundle"; public final static boolean IS_MAC; private static boolean isIOS; public static boolean blockNativeBrowser; private static final boolean isWindows; private static String fontFaceSystem; private Boolean darkMode; + private AutoLocalizationBundle autoLocalizationBundle; + private boolean autoUpdateDefaultResourceBundle; /** * @return the fullScreen @@ -227,6 +230,12 @@ private JFrame findTopFrame() { @Override public boolean isFullScreenSupported() { Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class); + autoUpdateDefaultResourceBundle = pref.getBoolean(PREF_AUTO_UPDATE_DEFAULT_BUNDLE, false); + if (autoUpdateDefaultResourceBundle) { + enableAutoLocalizationBundle(); + } else { + disableAutoLocalizationBundle(); + } boolean desktopSkin = pref.getBoolean("desktopSkin", false); if (isSimulator() && !desktopSkin) { return false; @@ -3484,6 +3493,24 @@ public void itemStateChanged(ItemEvent e) { }); simulatorMenu.add(useAppFrameMenu); + final JCheckBoxMenuItem autoLocalizationMenu = new JCheckBoxMenuItem("Auto Update Default Bundle"); + autoLocalizationMenu.setSelected(autoUpdateDefaultResourceBundle); + autoLocalizationMenu.addItemListener(new ItemListener() { + + @Override + public void itemStateChanged(ItemEvent e) { + boolean selected = autoLocalizationMenu.isSelected(); + autoUpdateDefaultResourceBundle = selected; + pref.putBoolean(PREF_AUTO_UPDATE_DEFAULT_BUNDLE, selected); + if (selected) { + enableAutoLocalizationBundle(); + } else { + disableAutoLocalizationBundle(); + } + } + }); + simulatorMenu.add(autoLocalizationMenu); + final JCheckBoxMenuItem zoomMenu = new JCheckBoxMenuItem("Zoom", scrollableSkin); if (appFrame == null) simulatorMenu.add(zoomMenu); @@ -5281,6 +5308,12 @@ public void init(Object m) { }*/ Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class); + autoUpdateDefaultResourceBundle = pref.getBoolean(PREF_AUTO_UPDATE_DEFAULT_BUNDLE, false); + if (autoUpdateDefaultResourceBundle) { + enableAutoLocalizationBundle(); + } else { + disableAutoLocalizationBundle(); + } boolean desktopSkin = pref.getBoolean("desktopSkin", false); if (desktopSkin && m == null) { safeAreaLandscape = null; @@ -13623,7 +13656,260 @@ public Boolean canExecute(String url) { return super.canExecute(url); } - + + private void enableAutoLocalizationBundle() { + File bundleFile = findDefaultLocalizationBundleFile(); + if (bundleFile == null) { + return; + } + Map current = UIManager.getInstance().getBundle(); + if (current instanceof AutoLocalizationBundle) { + AutoLocalizationBundle existing = (AutoLocalizationBundle) current; + if (existing.isForFile(bundleFile)) { + autoLocalizationBundle = existing; + existing.ensureFileExists(); + return; + } + } + AutoLocalizationBundle newBundle = new AutoLocalizationBundle(bundleFile, current); + autoLocalizationBundle = newBundle; + UIManager.getInstance().setBundle(newBundle); + } + + private void disableAutoLocalizationBundle() { + Map current = UIManager.getInstance().getBundle(); + if (current instanceof AutoLocalizationBundle) { + AutoLocalizationBundle existing = (AutoLocalizationBundle) current; + Map snapshot = new HashMap(existing); + UIManager.getInstance().setBundle(snapshot); + } + autoLocalizationBundle = null; + } + + private File findDefaultLocalizationBundleFile() { + File localizationDir = findLocalizationDirectory(); + if (localizationDir == null) { + return null; + } + java.util.List bundles = new java.util.ArrayList(); + collectLocalizationBundles(localizationDir, bundles); + File preferred = new File(localizationDir, "Bundle.properties"); + if (preferred.exists()) { + return preferred; + } + File best = null; + for (File f : bundles) { + String name = f.getName(); + if (!name.endsWith(".properties")) { + continue; + } + String base = name.substring(0, name.length() - ".properties".length()); + if (base.indexOf('_') < 0) { + if (best == null) { + best = f; + } + if ("Bundle".equals(base)) { + best = f; + break; + } + } + } + if (best != null) { + return best; + } + if (!bundles.isEmpty()) { + java.util.Collections.sort(bundles); + return bundles.get(0); + } + return preferred; + } + + private void collectLocalizationBundles(File dir, java.util.List out) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + collectLocalizationBundles(f, out); + } else if (f.getName().endsWith(".properties")) { + out.add(f); + } + } + } + + private File findLocalizationDirectory() { + File projectDir = getCWD(); + File[] candidates = new File[]{ + new File(projectDir, "src" + File.separator + "main" + File.separator + "l10n"), + new File(projectDir, "l10n"), + new File(projectDir, "src" + File.separator + "l10n") + }; + for (File dir : candidates) { + if (dir.exists() && dir.isDirectory()) { + return dir; + } + } + File fallback = candidates[0]; + if (!fallback.exists()) { + fallback.mkdirs(); + } + if (fallback.exists() && fallback.isDirectory()) { + return fallback; + } + return null; + } + + private static class AutoLocalizationBundle extends Hashtable { + private static final long serialVersionUID = 1L; + + private File bundleFile; + private final java.util.Properties properties = new java.util.Properties(); + private boolean dirty; + + AutoLocalizationBundle(File bundleFile, Map base) { + this.bundleFile = bundleFile; + ensureParentExists(); + if (bundleFile.exists()) { + loadFromFile(); + } else { + persist(); + } + if (base != null && !base.isEmpty()) { + for (Map.Entry entry : base.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + putInternal(entry.getKey(), entry.getValue()); + storeEntry(entry.getKey(), entry.getValue(), false); + } + persistIfNeeded(true); + } + } + + private void loadFromFile() { + try (InputStream in = new FileInputStream(bundleFile)) { + properties.clear(); + properties.load(in); + super.clear(); + for (String name : properties.stringPropertyNames()) { + putInternal(name, properties.getProperty(name)); + } + dirty = false; + } catch (IOException err) { + Log.e(err); + } + } + + private void ensureParentExists() { + File parent = bundleFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + } + + private void persist() { + ensureParentExists(); + try (OutputStream out = new FileOutputStream(bundleFile)) { + properties.store(out, "Codename One auto-generated bundle"); + } catch (IOException err) { + Log.e(err); + } + dirty = false; + } + + private void persistIfNeeded(boolean force) { + if (force) { + if (dirty || !bundleFile.exists()) { + persist(); + } else { + ensureParentExists(); + } + } else if (dirty) { + persist(); + } + } + + private void storeEntry(String key, String value, boolean flush) { + if (key == null || value == null) { + return; + } + String current = properties.getProperty(key); + if (current == null || !current.equals(value)) { + properties.setProperty(key, value); + dirty = true; + } + if (flush) { + persistIfNeeded(true); + } + } + + private String putInternal(String key, String value) { + return (String) super.put(key, value); + } + + @Override + public synchronized String get(Object key) { + String value = super.get(key); + if (key instanceof String) { + String strKey = (String) key; + if (value == null) { + String autoValue = strKey; + putInternal(strKey, autoValue); + storeEntry(strKey, autoValue, true); + value = autoValue; + } + } + return value; + } + + @Override + public synchronized String put(String key, String value) { + String result = putInternal(key, value); + storeEntry(key, value, true); + return result; + } + + @Override + public synchronized String remove(Object key) { + String result = super.remove(key); + if (key instanceof String) { + if (properties.containsKey(key)) { + properties.remove(key); + dirty = true; + persistIfNeeded(true); + } else { + ensureParentExists(); + } + } + return result; + } + + @Override + public synchronized void clear() { + super.clear(); + if (!properties.isEmpty()) { + properties.clear(); + dirty = true; + persistIfNeeded(true); + } else { + ensureParentExists(); + } + } + + boolean isForFile(File file) { + return bundleFile.equals(file); + } + + void ensureFileExists() { + ensureParentExists(); + if (!bundleFile.exists()) { + persist(); + } + } + } + + public static File getCWD() { return new File(System.getProperty("user.dir")); } diff --git a/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java b/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java new file mode 100644 index 0000000000..d25aeb30ee --- /dev/null +++ b/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java @@ -0,0 +1,98 @@ +package com.codename1.impl.javase; + +import com.codename1.testing.AbstractTest; +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Tests for the auto-updating localization bundle used by the Java SE simulator. + */ +public class AutoLocalizationBundleTest extends AbstractTest { + + @Override + public boolean runTest() throws Exception { + File tempDir = File.createTempFile("cn1-auto-bundle", ""); + if (tempDir.exists() && !tempDir.delete()) { + throw new RuntimeException("Failed to delete temp file " + tempDir); + } + if (!tempDir.mkdirs()) { + throw new RuntimeException("Failed to create temp directory " + tempDir); + } + + File bundleDir = new File(tempDir, "nested"); + File bundleFile = new File(bundleDir, "Bundle.properties"); + + Map base = new HashMap(); + base.put("hello", "world"); + + Class bundleClass = Class.forName("com.codename1.impl.javase.JavaSEPort$AutoLocalizationBundle"); + Constructor ctor = bundleClass.getDeclaredConstructor(File.class, Map.class); + ctor.setAccessible(true); + + try { + Object bundle = ctor.newInstance(bundleFile, base); + @SuppressWarnings("unchecked") + Map bundleMap = (Map) bundle; + + assertTrue(bundleFile.exists(), "Bundle file should have been created"); + + Properties props = load(bundleFile); + assertEqual("world", props.getProperty("hello"), "Base entries should be written to the bundle file"); + + String generated = bundleMap.get("missingKey"); + assertEqual("missingKey", generated, "Missing lookups should generate default values"); + + props = load(bundleFile); + assertEqual("missingKey", props.getProperty("missingKey"), "Generated entry should be persisted"); + + bundleMap.put("hello", "updated"); + props = load(bundleFile); + assertEqual("updated", props.getProperty("hello"), "Explicit put should persist new value"); + + bundleMap.remove("hello"); + props = load(bundleFile); + assertNull(props.getProperty("hello"), "Removed keys should be deleted from the bundle file"); + + Object bundleReloaded = ctor.newInstance(bundleFile, null); + @SuppressWarnings("unchecked") + Map bundleReloadedMap = (Map) bundleReloaded; + assertEqual("missingKey", bundleReloadedMap.get("missingKey"), "Existing persisted values should be loaded"); + + return true; + } finally { + deleteRecursive(tempDir); + } + } + + private Properties load(File file) throws Exception { + Properties props = new Properties(); + if (file.exists()) { + FileInputStream fis = new FileInputStream(file); + try { + props.load(fis); + } finally { + fis.close(); + } + } + return props; + } + + private void deleteRecursive(File file) { + if (file == null || !file.exists()) { + return; + } + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursive(child); + } + } + } + file.delete(); + } +}