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
72 changes: 53 additions & 19 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -14337,8 +14337,18 @@ private void disableAutoLocalizationBundle() {
autoLocalizationBundle = null;
}

private File findDefaultLocalizationBundleFile() {
File localizationDir = findLocalizationDirectory();
File findDefaultLocalizationBundleFile() {
return findDefaultLocalizationBundleFile(getCWD());
}

// Returns null when no real bundle file exists. Previously this method
// returned a non-existent `Bundle.properties` path as a fallback, which
// combined with `findLocalizationDirectory`'s old mkdirs() to create empty
// ghost bundles in modules the developer never asked to localize. Now the
// auto-bundle activates only when the developer ships at least one
// bundle file, matching the project-level opt-in semantics.
static File findDefaultLocalizationBundleFile(File projectDir) {
File localizationDir = findLocalizationDirectory(projectDir);
if (localizationDir == null) {
return null;
}
Expand Down Expand Up @@ -14372,10 +14382,10 @@ private File findDefaultLocalizationBundleFile() {
java.util.Collections.sort(bundles);
return bundles.get(0);
}
return preferred;
return null;
}

private void collectLocalizationBundles(File dir, java.util.List<File> out) {
private static void collectLocalizationBundles(File dir, java.util.List<File> out) {
File[] files = dir.listFiles();
if (files == null) {
return;
Expand All @@ -14389,25 +14399,49 @@ private void collectLocalizationBundles(File dir, java.util.List<File> out) {
}
}

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")
};
File findLocalizationDirectory() {
return findLocalizationDirectory(getCWD());
}

// Resolves the project's localization bundle directory for the auto-bundle.
//
// The simulator forks `cn1:run` from the `javase/` module, so cwd is `javase/`
// -- but the developer's bundles live in the sibling `common/` module under
// `common/src/main/l10n`. Issue #4850: previous versions of this method
// looked only at cwd, missed the real bundle, and then auto-created a
// throwaway `javase/src/main/l10n/Bundle.properties` via mkdirs(). The
// throwaway file accumulated wormhole-poisoned `@im=@im` entries from older
// CN1 versions; even after users cleaned the *real* bundle in `common/`,
// every CN1CSSCLI subprocess respawn loaded the ghost bundle from `javase/`,
// crashed inside `parseTextFieldInputMode`, and CSSWatcher restarted it in
// an infinite respawn loop.
//
// New rules:
// 1. Check the current module first (cwd/src/main/{l10n,i18n}).
// 2. Then check the sibling `common/` module (matches CN1 maven layout
// and CSSWatcher.addLocalizationCandidates).
// 3. Never auto-create the directory. Project-level opt-in: if the
// developer hasn't set up localization, the auto-bundle is a no-op.
static File findLocalizationDirectory(File projectDir) {
if (projectDir == null) {
return null;
}
java.util.List<File> candidates = new java.util.ArrayList<File>();
candidates.add(new File(projectDir, "src" + File.separator + "main" + File.separator + "l10n"));
candidates.add(new File(projectDir, "src" + File.separator + "main" + File.separator + "i18n"));
candidates.add(new File(projectDir, "l10n"));
candidates.add(new File(projectDir, "src" + File.separator + "l10n"));
File parent = projectDir.getParentFile();
if (parent != null) {
File commonModule = new File(parent, "common");
candidates.add(new File(commonModule, "src" + File.separator + "main" + File.separator + "l10n"));
candidates.add(new File(commonModule, "src" + File.separator + "main" + File.separator + "i18n"));
}
for (File dir : candidates) {
if (dir.exists() && dir.isDirectory()) {
if (dir != null && dir.exists() && dir.isDirectory()) {
return dir;
}
}
File fallback = candidates[0];
if (!fallback.exists()) {
fallback.mkdirs();
}
if (fallback.exists() && fallback.isDirectory()) {
return fallback;
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
Expand Down Expand Up @@ -82,6 +83,9 @@ public boolean runTest() throws Exception {

verifySetBundleSmokeOnFreshProject(ctor, tempDir);
verifySetBundleHealsLegacyWormholeFile(ctor, tempDir);
verifyFindLocalizationDirectoryDoesNotAutoCreate(tempDir);
verifyFindLocalizationDirectoryWalksToCommonSibling(tempDir);
verifyFindDefaultBundleReturnsNullWhenNoBundleFile(tempDir);

return true;
} finally {
Expand Down Expand Up @@ -186,6 +190,85 @@ private void verifySetBundleHealsLegacyWormholeFile(Constructor<?> ctor, File te
}
}

/// Issue #4850 root cause: `findLocalizationDirectory` used to call
/// `mkdirs()` on `<cwd>/src/main/l10n` whenever the directory was missing,
/// then `findDefaultLocalizationBundleFile` returned a non-existent
/// `Bundle.properties` path as a fallback. The CSS subprocess inherits cwd
/// = `javase/`, so this auto-created a ghost bundle in the wrong module
/// that older CN1 versions then poisoned with `@im=@im`. After this fix,
/// no l10n dir on disk = no auto-bundle (project-level opt-in).
private void verifyFindLocalizationDirectoryDoesNotAutoCreate(File tempDir) throws Exception {
File freshModule = new File(tempDir, "no-l10n-module");
if (!freshModule.mkdirs()) {
throw new RuntimeException("Failed to create test module dir " + freshModule);
}

Method findLocDir = Class.forName("com.codename1.impl.javase.JavaSEPort")
.getDeclaredMethod("findLocalizationDirectory", File.class);
findLocDir.setAccessible(true);

Object result = findLocDir.invoke(null, freshModule);
assertNull(result, "findLocalizationDirectory must return null when no l10n dir exists");

File ghostDir = new File(freshModule, "src" + File.separator + "main" + File.separator + "l10n");
assertBool(!ghostDir.exists(), "findLocalizationDirectory must not auto-create src/main/l10n");
}

/// Issue #4850: the simulator forks `cn1:run` from `javase/` while the
/// developer's bundles live in the sibling `common/` module. The new
/// `findLocalizationDirectory` walks up to find `../common/src/main/l10n`
/// when the current module has no l10n dir of its own, mirroring
/// `CSSWatcher.addLocalizationCandidates`.
private void verifyFindLocalizationDirectoryWalksToCommonSibling(File tempDir) throws Exception {
File rootProject = new File(tempDir, "multi-module-project");
File javaseModule = new File(rootProject, "javase");
File commonL10n = new File(rootProject, "common" + File.separator + "src" + File.separator + "main" + File.separator + "l10n");
if (!javaseModule.mkdirs() || !commonL10n.mkdirs()) {
throw new RuntimeException("Failed to create multi-module project layout under " + rootProject);
}

Method findLocDir = Class.forName("com.codename1.impl.javase.JavaSEPort")
.getDeclaredMethod("findLocalizationDirectory", File.class);
findLocDir.setAccessible(true);

File result = (File) findLocDir.invoke(null, javaseModule);
assertNotNull(result, "findLocalizationDirectory must locate sibling common/src/main/l10n");
assertEqual(commonL10n.getCanonicalPath(), result.getCanonicalPath(),
"findLocalizationDirectory should resolve to the common module's l10n dir when cwd is javase");

// Local module wins when both exist.
File javaseL10n = new File(javaseModule, "src" + File.separator + "main" + File.separator + "l10n");
if (!javaseL10n.mkdirs()) {
throw new RuntimeException("Failed to create local l10n dir " + javaseL10n);
}
File preferLocal = (File) findLocDir.invoke(null, javaseModule);
assertEqual(javaseL10n.getCanonicalPath(), preferLocal.getCanonicalPath(),
"Local module's l10n dir should take precedence over common");
}

/// `findDefaultLocalizationBundleFile` previously returned a non-existent
/// `Bundle.properties` path when the dir was empty; that triggered
/// `AutoLocalizationBundle.persist()` to create the empty file even when
/// the project shipped no bundles. Now it returns null and
/// `enableAutoLocalizationBundle` no-ops.
private void verifyFindDefaultBundleReturnsNullWhenNoBundleFile(File tempDir) throws Exception {
File emptyL10nModule = new File(tempDir, "empty-l10n-module");
File emptyL10n = new File(emptyL10nModule, "src" + File.separator + "main" + File.separator + "l10n");
if (!emptyL10n.mkdirs()) {
throw new RuntimeException("Failed to create empty l10n dir " + emptyL10n);
}

Method findDefaultBundle = Class.forName("com.codename1.impl.javase.JavaSEPort")
.getDeclaredMethod("findDefaultLocalizationBundleFile", File.class);
findDefaultBundle.setAccessible(true);

Object result = findDefaultBundle.invoke(null, emptyL10nModule);
assertNull(result, "findDefaultLocalizationBundleFile must return null when the l10n dir has no .properties files");

File preferred = new File(emptyL10n, "Bundle.properties");
assertBool(!preferred.exists(), "findDefaultLocalizationBundleFile must not create Bundle.properties");
}

private Properties load(File file) throws Exception {
Properties props = new Properties();
if (file.exists()) {
Expand Down
Loading