From 48eeac9f7d06013d219a2fb62ccbd05bc7b63b43 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 2 May 2026 18:15:23 +0300 Subject: [PATCH 1/5] Fix initializr localization bundles + harden simulator designer lookup The initializr's "include localization bundles" option generated bundles under common/src/main/resources/messages*.properties, but the CN1 maven plugin's CSS compiler scans common/src/main/l10n (or i18n) for bundles to bake into theme.res. The result: Resources.getGlobalResources().getL10N( "messages", lang) hit a missing resource id at simulator startup and threw NPE in MyAppName.init -- the project couldn't run. - GeneratorModel.addLocalizationEntries: write to src/main/l10n so the bundles actually end up inside theme.res. - Bootstrap (Java + Kotlin) injected into the starter class is now null-safe and falls back to the default locale when the device language has no specific bundle. - Resources.getL10N / listL10NLocales / l10NLocaleSet now return null instead of NPE-ing when the bundle id is absent. Defensive change at the framework level so any project shipping mismatched bundles degrades gracefully. - New tests/core/.../ResourcesL10NTest covers the framework null-safety. - GeneratorModelMatrixTest now asserts bundles land under l10n and are NOT under src/main/resources (catches the regression at unit-test time). - GeneratorModelIntegrationBuildTest now opens common/target/classes/ theme.res after mvn compile and verifies "messages" L10N data is present for both the default ("") and Hebrew ("he") locales -- the end-to-end signal the previous tests missed. While here, harden the simulator's CSS compiler invocation against stale ~/.codenameone/designer_1.jar: - New MavenUtils.findDesignerJarInM2 derives the running CN1 version from the codenameone-core jar's m2 path and resolves the matching codenameone-designer--jar-with-dependencies.jar. Any plugin invocation has already pulled this into m2 as a plugin dependency. - CSSWatcher and ComponentTreeInspector now prefer codename1.designer.jar -> m2 designer -> ~/.codenameone fallback (with a clear warning when the legacy fallback is hit). The build-time CSS goal already used getDesignerJar() so this only affects the simulator runtime / live CSS reload paths. - CompileCSSMojo now invokes the forked CSS compiler with INFO log level instead of DEBUG so subprocess stack traces are visible without re-running with -X. This won't fix issue #4850 but makes the next similar report actionable. - The four initializr pom templates replace skipexisting="true" with usetimestamp="true" on the UpdateCodenameOne.jar download so future installs refresh the updater jar instead of pinning forever to the first copy. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/ui/util/Resources.java | 18 ++++++-- .../com/codename1/impl/javase/CSSWatcher.java | 16 +++++++ .../impl/javase/ComponentTreeInspector.java | 34 +++++++++----- .../impl/javase/util/MavenUtils.java | 45 +++++++++++++++++++ .../com/codename1/maven/CompileCSSMojo.java | 6 ++- .../initializr/model/GeneratorModel.java | 28 +++++++++--- .../src/main/resources/barebones-pom.xml | 8 +++- .../common/src/main/resources/grub-pom.xml | 4 +- .../common/src/main/resources/kotlin-pom.xml | 4 +- .../common/src/main/resources/tweet-pom.xml | 4 +- .../GeneratorModelIntegrationBuildTest.java | 35 +++++++++++++++ .../model/GeneratorModelMatrixTest.java | 19 +++++--- .../codename1/ui/util/ResourcesL10NTest.java | 37 +++++++++++++++ 13 files changed, 225 insertions(+), 33 deletions(-) create mode 100644 tests/core/src/com/codename1/ui/util/ResourcesL10NTest.java diff --git a/CodenameOne/src/com/codename1/ui/util/Resources.java b/CodenameOne/src/com/codename1/ui/util/Resources.java index 410b23c698..c1104d8b08 100644 --- a/CodenameOne/src/com/codename1/ui/util/Resources.java +++ b/CodenameOne/src/com/codename1/ui/util/Resources.java @@ -929,7 +929,11 @@ InputStream getUi(String id) { /// /// Hashtable containing key value pairs for localized data public Hashtable getL10N(String id, String locale) { - return (Hashtable) ((Hashtable) resources.get(id)).get(locale); + Hashtable bundles = (Hashtable) resources.get(id); + if (bundles == null) { + return null; + } + return (Hashtable) bundles.get(locale); } /// Returns an enumration of the locales supported by this resource id @@ -942,7 +946,11 @@ public Hashtable getL10N(String id, String locale) { /// /// enumeration of strings containing bundle names public Enumeration listL10NLocales(String id) { - return ((Hashtable) resources.get(id)).keys(); + Hashtable bundles = (Hashtable) resources.get(id); + if (bundles == null) { + return null; + } + return bundles.keys(); } /// Returns a collection of the l10 locale names @@ -955,7 +963,11 @@ public Enumeration listL10NLocales(String id) { /// /// collection of strings containing bundle names public Collection l10NLocaleSet(String id) { - return ((Hashtable) resources.get(id)).keySet(); + Hashtable bundles = (Hashtable) resources.get(id); + if (bundles == null) { + return null; + } + return bundles.keySet(); } /// Returns the font resource from the file diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java b/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java index 6141000822..b54fdb6508 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java @@ -272,6 +272,22 @@ public void run() { File designerJar = new File(cn1Home, "designer_1.jar"); if (System.getProperty("codename1.designer.jar", null) != null) { designerJar = new File(System.getProperty("codename1.designer.jar", null)); + } else { + // The Maven plugin declares codenameone-designer as a plugin dependency, so any + // CN1 mojo invocation has already pulled the version-pinned designer jar into m2. + // Prefer that over ~/.codenameone/designer_1.jar (which is managed by UpdateCodenameOne + // and routinely lags behind the plugin version, producing confusing CSS failures). + File m2Designer = com.codename1.impl.javase.util.MavenUtils.findDesignerJarInM2(); + if (m2Designer != null) { + designerJar = m2Designer; + } else if (designerJar.exists()) { + System.err.println("[CSSWatcher] Warning: codename1.designer.jar system property is not set " + + "and no version-pinned designer was found in the local Maven repository; " + + "falling back to " + designerJar.getAbsolutePath() + ". This file is " + + "managed by UpdateCodenameOne and may be older than the CN1 plugin in use. " + + "If CSS compilation fails, launch the simulator via the Maven cn1:run goal " + + "(which both fetches the right designer into m2 and pins it via -Dcodename1.designer.jar)."); + } } String cefDir = System.getProperty("cef.dir", cn1Home + File.separator + "cef"); diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java index 78fad4e539..cc01629552 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java @@ -580,18 +580,32 @@ private void findScrollableContainers(Container cnt, List response) { } private void editStyle() { - File cn1dir = new File(System.getProperty("user.home"), ".codenameone"); - if(!cn1dir.exists()) { - JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE); - return; + // Prefer the version-pinned designer jar that the Maven plugin pulled into m2. + // Fallback to the legacy ~/.codenameone/designer_*.jar files (managed by UpdateCodenameOne). + File resourceEditor = null; + if (System.getProperty("codename1.designer.jar", null) != null) { + resourceEditor = new File(System.getProperty("codename1.designer.jar")); } - File resourceEditor = new File(cn1dir, "designer_1.jar"); - if(!resourceEditor.exists()) { - resourceEditor = new File(cn1dir, "designer.jar"); + if (resourceEditor == null || !resourceEditor.exists()) { + File m2Designer = com.codename1.impl.javase.util.MavenUtils.findDesignerJarInM2(); + if (m2Designer != null) { + resourceEditor = m2Designer; + } } - if(!resourceEditor.exists()) { - JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE); - return; + if (resourceEditor == null || !resourceEditor.exists()) { + File cn1dir = new File(System.getProperty("user.home"), ".codenameone"); + if(!cn1dir.exists()) { + JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE); + return; + } + resourceEditor = new File(cn1dir, "designer_1.jar"); + if(!resourceEditor.exists()) { + resourceEditor = new File(cn1dir, "designer.jar"); + } + if(!resourceEditor.exists()) { + JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE); + return; + } } File javaBin = new File(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java.exe"); diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java b/Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java index 24a1c6456e..d807cca2f5 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java @@ -6,7 +6,9 @@ package com.codename1.impl.javase.util; import com.codename1.io.Log; +import com.codename1.ui.Display; import java.io.File; +import java.net.URL; /** * @@ -63,6 +65,49 @@ public static File findJavac() { return null; } + /** + * Locate the codenameone-designer:jar-with-dependencies jar inside the local + * Maven (~/.m2) repository, using the version of the codenameone-core jar that + * is currently loaded into this JVM. Returns null if the running framework is + * not loaded from m2 (e.g. running from a build directory) or if the matching + * designer jar has not been resolved yet. + * + *

The Maven plugin declares codenameone-designer as a plugin dependency, so + * any plugin invocation (cn1:run, mvn compile when bound to the css goal, etc.) + * implicitly fetches the matching designer jar into m2. This lookup lets the + * simulator runtime use that exact version even when codename1.designer.jar + * isn't passed as a system property -- avoiding a stale ~/.codenameone/designer_1.jar + * fallback. + */ + public static File findDesignerJarInM2() { + try { + URL location = Display.class.getProtectionDomain().getCodeSource().getLocation(); + if (location == null) { + return null; + } + File coreJar = new File(location.toURI()); + // Expected layout: /com/codenameone/codenameone-core//codenameone-core-.jar + File versionDir = coreJar.getParentFile(); + if (versionDir == null) return null; + File coreDir = versionDir.getParentFile(); + if (coreDir == null) return null; + File codenameoneGroupDir = coreDir.getParentFile(); + if (codenameoneGroupDir == null) return null; + if (!"codenameone-core".equals(coreDir.getName())) { + return null; + } + String version = versionDir.getName(); + File designerVersionDir = new File(codenameoneGroupDir, "codenameone-designer" + File.separator + version); + File designer = new File(designerVersionDir, "codenameone-designer-" + version + "-jar-with-dependencies.jar"); + if (designer.isFile()) { + return designer; + } + } catch (Throwable t) { + // Best-effort lookup. Any unexpected layout means we can't resolve via m2. + } + return null; + } + public static boolean isRunningInJDK() { if (!isRunningInJDKChecked) { isRunningInJDKChecked = true; diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java index 02cdb8f3db..d2d3009c89 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java @@ -237,7 +237,11 @@ private void executeImpl(String themePrefix) throws MojoExecutionException, Mojo // zip file (which is the designer jar with all dependencies). We use this jar // rather than the central designer_1.jar located in the user's home directory to make it // easier to pin to a particular version. - Java java = createJava(); + // Use INFO log level (rather than the default DEBUG) so that stack traces from the + // forked CSS compiler are visible without re-running with -X. When the subprocess + // throws (e.g. StringIndexOutOfBoundsException in CN1CSSCLI), users currently only see + // the wrapper "An error occurred while compiling the CSS files" with no useful detail. + Java java = createJava(org.apache.maven.doxia.logging.Log.LEVEL_INFO); java.setDir(getCN1ProjectDir()); java.setJar(getDesignerJar()); java.setFork(true); diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index 386604fe80..b8755b5b01 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -106,8 +106,12 @@ private void addLocalizationEntries(Map mergedEntries) throws IO if (!isBareTemplate() || !options.includeLocalizationBundles) { return; } + // The Codename One Maven plugin's CSS compiler scans src/main/l10n (or src/main/i18n) + // for *.properties bundles and bakes them into theme.res. If the bundles are placed + // anywhere else (e.g. src/main/resources) they are NOT baked into the resource file + // and Resources.getGlobalResources().getL10N("messages", lang) returns null at runtime. copySingleTextEntryToMap( - "common/src/main/resources/messages.properties", + "common/src/main/l10n/messages.properties", readResourceToString("/messages.properties"), mergedEntries, ZipEntryType.COMMON @@ -117,7 +121,7 @@ private void addLocalizationEntries(Map mergedEntries) throws IO continue; } copySingleTextEntryToMap( - "common/src/main/resources/messages_" + language.bundleSuffix + ".properties", + "common/src/main/l10n/messages_" + language.bundleSuffix + ".properties", readResourceToString("/messages_" + language.bundleSuffix + ".properties"), mergedEntries, ZipEntryType.COMMON @@ -356,8 +360,14 @@ private String injectJavaLocalizationBootstrap(String content) { + " public void init(Object context) {\n" + " super.init(context);\n" + " String language = L10NManager.getInstance().getLanguage();\n" - + " Hashtable bundle = Resources.getGlobalResources().getL10N(\"messages\", language);\n" - + " UIManager.getInstance().setBundle(bundle);\n" + + " Resources global = Resources.getGlobalResources();\n" + + " Hashtable bundle = global == null ? null : global.getL10N(\"messages\", language);\n" + + " if (bundle == null && global != null) {\n" + + " bundle = global.getL10N(\"messages\", \"\");\n" + + " }\n" + + " if (bundle != null) {\n" + + " UIManager.getInstance().setBundle(bundle);\n" + + " }\n" + " }\n\n"; int firstBrace = content.indexOf('{'); if (firstBrace > -1) { @@ -374,8 +384,14 @@ private String injectKotlinLocalizationBootstrap(String content) { String method = "\n override fun init(context: Any?) {\n" + " super.init(context)\n" + " val language = L10NManager.getInstance().language\n" - + " val bundle: Hashtable? = Resources.getGlobalResources().getL10N(\"messages\", language)\n" - + " UIManager.getInstance().setBundle(bundle)\n" + + " val global = Resources.getGlobalResources()\n" + + " var bundle: Hashtable? = global?.getL10N(\"messages\", language)\n" + + " if (bundle == null) {\n" + + " bundle = global?.getL10N(\"messages\", \"\")\n" + + " }\n" + + " if (bundle != null) {\n" + + " UIManager.getInstance().setBundle(bundle)\n" + + " }\n" + " }\n\n"; int firstBrace = content.indexOf('{'); if (firstBrace > -1) { diff --git a/scripts/initializr/common/src/main/resources/barebones-pom.xml b/scripts/initializr/common/src/main/resources/barebones-pom.xml index 45b112c92d..5367184adb 100644 --- a/scripts/initializr/common/src/main/resources/barebones-pom.xml +++ b/scripts/initializr/common/src/main/resources/barebones-pom.xml @@ -53,14 +53,18 @@ + diff --git a/scripts/initializr/common/src/main/resources/grub-pom.xml b/scripts/initializr/common/src/main/resources/grub-pom.xml index 2d0b619703..dabd70663a 100644 --- a/scripts/initializr/common/src/main/resources/grub-pom.xml +++ b/scripts/initializr/common/src/main/resources/grub-pom.xml @@ -62,12 +62,12 @@ diff --git a/scripts/initializr/common/src/main/resources/kotlin-pom.xml b/scripts/initializr/common/src/main/resources/kotlin-pom.xml index 012aacea7d..ca92d3951f 100644 --- a/scripts/initializr/common/src/main/resources/kotlin-pom.xml +++ b/scripts/initializr/common/src/main/resources/kotlin-pom.xml @@ -55,12 +55,12 @@ diff --git a/scripts/initializr/common/src/main/resources/tweet-pom.xml b/scripts/initializr/common/src/main/resources/tweet-pom.xml index 29d137a360..bb6d73b787 100644 --- a/scripts/initializr/common/src/main/resources/tweet-pom.xml +++ b/scripts/initializr/common/src/main/resources/tweet-pom.xml @@ -67,12 +67,12 @@ diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelIntegrationBuildTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelIntegrationBuildTest.java index 5779d205f6..46bd7289e2 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelIntegrationBuildTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelIntegrationBuildTest.java @@ -2,12 +2,14 @@ import com.codename1.io.Util; import com.codename1.testing.AbstractTest; +import com.codename1.ui.util.Resources; import net.sf.zipme.ZipEntry; import net.sf.zipme.ZipInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -15,6 +17,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.Map; @@ -65,6 +68,38 @@ private void buildGeneratedProject(ProjectOptions.JavaVersion version, Path java int exitCode = runMavenCompile(projectDir, homeDir, javaHome); assertTrue(exitCode == 0, "Generated project should compile with selected JDK. Version=" + version.label + " | exitCode=" + exitCode); + + // Localization bundles were requested -- they must end up baked into theme.res so + // that Resources.getGlobalResources().getL10N("messages", lang) resolves at runtime. + // This is the regression test for the NPE in MyAppName.init() reported when bundles + // were generated under common/src/main/resources instead of common/src/main/l10n. + assertLocalizationBakedIntoThemeRes(projectDir, version); + } + + private void assertLocalizationBakedIntoThemeRes(Path projectDir, ProjectOptions.JavaVersion version) throws Exception { + Path themeRes = projectDir.resolve("common/target/classes/theme.res"); + assertTrue(Files.isRegularFile(themeRes), + "theme.res should exist after compile. Version=" + version.label + " | path=" + themeRes); + + Resources res; + try (FileInputStream in = new FileInputStream(themeRes.toFile())) { + res = Resources.open(in); + } + + Hashtable defaultBundle = res.getL10N("messages", ""); + assertNotNull(defaultBundle, + "theme.res should contain a 'messages' L10N bundle for the default locale (\"\"). " + + "If null, bundles were not picked up by the CN1 css compiler -- check that " + + "they are placed under common/src/main/l10n. Version=" + version.label); + assertTrue(defaultBundle.size() > 0, + "Default 'messages' bundle should not be empty. Version=" + version.label); + + Hashtable hebrew = res.getL10N("messages", "he"); + assertNotNull(hebrew, + "theme.res should contain a Hebrew 'messages' bundle when localization bundles are requested. " + + "Version=" + version.label); + assertTrue(hebrew.size() > 0, + "Hebrew 'messages' bundle should not be empty. Version=" + version.label); } private byte[] createProjectZip(ProjectOptions options, String appName, String packageName) throws IOException { diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index b3a1e87c31..2b7904a021 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -345,18 +345,27 @@ private void assertMainSourceFile(Map entries, Template template private void assertLocalizationBundles(Map entries, Template template, boolean expectLocalizationBundles) { + // Bundles MUST live under common/src/main/l10n -- that's where the CN1 maven plugin's + // CSS compiler scans for properties files to bake into theme.res. Placing them under + // src/main/resources causes Resources.getGlobalResources().getL10N("messages", lang) + // to return null at runtime (see CompileCSSMojo.findLocalizationDirectory). if (template == Template.BAREBONES || template == Template.KOTLIN) { if (expectLocalizationBundles) { - assertNotNull(entries.get("common/src/main/resources/messages.properties"), "Barebones templates should include default localization bundle"); - assertNotNull(entries.get("common/src/main/resources/messages_ar.properties"), "Barebones templates should include Arabic localization bundle"); - assertNotNull(entries.get("common/src/main/resources/messages_he.properties"), "Barebones templates should include Hebrew localization bundle"); + assertNotNull(entries.get("common/src/main/l10n/messages.properties"), "Barebones templates should include default localization bundle under l10n"); + assertNotNull(entries.get("common/src/main/l10n/messages_ar.properties"), "Barebones templates should include Arabic localization bundle under l10n"); + assertNotNull(entries.get("common/src/main/l10n/messages_he.properties"), "Barebones templates should include Hebrew localization bundle under l10n"); + assertNull(entries.get("common/src/main/resources/messages.properties"), "Bundles must not be written to src/main/resources -- the CN1 plugin will not bake them into theme.res"); + assertNull(entries.get("common/src/main/resources/messages_ar.properties"), "Bundles must not be written to src/main/resources -- the CN1 plugin will not bake them into theme.res"); + assertNull(entries.get("common/src/main/resources/messages_he.properties"), "Bundles must not be written to src/main/resources -- the CN1 plugin will not bake them into theme.res"); } else { + assertNull(entries.get("common/src/main/l10n/messages.properties"), "Barebones templates should not include localization bundles by default"); + assertNull(entries.get("common/src/main/l10n/messages_ar.properties"), "Barebones templates should not include Arabic localization bundle by default"); + assertNull(entries.get("common/src/main/l10n/messages_he.properties"), "Barebones templates should not include Hebrew localization bundle by default"); assertNull(entries.get("common/src/main/resources/messages.properties"), "Barebones templates should not include localization bundles by default"); - assertNull(entries.get("common/src/main/resources/messages_ar.properties"), "Barebones templates should not include Arabic localization bundle by default"); - assertNull(entries.get("common/src/main/resources/messages_he.properties"), "Barebones templates should not include Hebrew localization bundle by default"); } return; } + assertNull(entries.get("common/src/main/l10n/messages.properties"), "Non-bare templates should not receive default localization bundle"); assertNull(entries.get("common/src/main/resources/messages.properties"), "Non-bare templates should not receive default localization bundle"); } diff --git a/tests/core/src/com/codename1/ui/util/ResourcesL10NTest.java b/tests/core/src/com/codename1/ui/util/ResourcesL10NTest.java new file mode 100644 index 0000000000..7b9a08425c --- /dev/null +++ b/tests/core/src/com/codename1/ui/util/ResourcesL10NTest.java @@ -0,0 +1,37 @@ +package com.codename1.ui.util; + +import com.codename1.testing.AbstractTest; + +import java.util.Hashtable; + +/** + * Regression test for Resources.getL10N / listL10NLocales / l10NLocaleSet + * returning null instead of throwing NullPointerException when a bundle id + * is not present in the .res file. + * + * Reported when an initializr-generated barebones project shipped its bundles + * under common/src/main/resources instead of common/src/main/l10n. The runtime + * lookup blew up at MyAppName.init -> Resources.getL10N because the resource id + * was missing from theme.res entirely. + */ +public class ResourcesL10NTest extends AbstractTest { + + @Override + public boolean runTest() throws Exception { + Resources empty = new Resources(); + + Hashtable bundle = empty.getL10N("missingBundle", "en"); + assertTrue(bundle == null, "getL10N must return null for an unknown bundle id, not throw NPE"); + + bundle = empty.getL10N("missingBundle", ""); + assertTrue(bundle == null, "getL10N must return null for an unknown bundle id with empty locale"); + + assertTrue(empty.listL10NLocales("missingBundle") == null, + "listL10NLocales must return null for an unknown bundle id, not throw NPE"); + + assertTrue(empty.l10NLocaleSet("missingBundle") == null, + "l10NLocaleSet must return null for an unknown bundle id, not throw NPE"); + + return true; + } +} From 314d7b249c9d5a31fcc526b1871fd998563cf7f5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 2 May 2026 18:25:25 +0300 Subject: [PATCH 2/5] CompileCSSMojo: route INFO log level through createJava() override The previous patch raised the CSS subprocess log level by passing it explicitly at the call site (createJava(LEVEL_INFO)), which bypasses CompileCSSMojoTest's TestCompileCSSMojo.createJava() override -- the test substitutes a RecordingJava there to capture the command line without forking. The override was no longer hit, so the test fell through to a real fork against a stub designer.jar and four tests errored out with "Invalid or corrupt jarfile". Move the INFO log level into a createJava() override on CompileCSSMojo itself. The call site stays at createJava(), so the test override continues to win, and production still gets INFO so subprocess stack traces remain visible without -X. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/codename1/maven/CompileCSSMojo.java | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java index d2d3009c89..25599bf105 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CompileCSSMojo.java @@ -38,11 +38,27 @@ * * @author shannah */ -@Mojo(name = "css", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, +@Mojo(name = "css", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) public class CompileCSSMojo extends AbstractCN1Mojo { + /** + * Override the default DEBUG log level so the forked CSS compiler's stdout + * is visible in normal mvn output. When the CSS subprocess throws (e.g. + * StringIndexOutOfBoundsException in CN1CSSCLI), users currently only see + * the wrapper "An error occurred while compiling the CSS files" message + * with no usable detail unless they re-run with -X. + * + * Routed through createJava() (not the call site) so subclasses that + * override createJava() in tests still get to substitute their recording + * Java task without having to know about the log level. + */ + @Override + public org.apache.tools.ant.taskdefs.Java createJava() { + return createJava(org.apache.maven.doxia.logging.Log.LEVEL_INFO); + } + @Override protected void executeImpl() throws MojoExecutionException, MojoFailureException { @@ -232,16 +248,15 @@ private void executeImpl(String themePrefix) throws MojoExecutionException, Mojo - // Run the CSS compiler which is contained inside the codenameone-designer jar + // Run the CSS compiler which is contained inside the codenameone-designer jar. // NOTE: The codenameone-designer.jar is a dependency of the codenameone-maven-plugin as // zip file (which is the designer jar with all dependencies). We use this jar // rather than the central designer_1.jar located in the user's home directory to make it // easier to pin to a particular version. - // Use INFO log level (rather than the default DEBUG) so that stack traces from the - // forked CSS compiler are visible without re-running with -X. When the subprocess - // throws (e.g. StringIndexOutOfBoundsException in CN1CSSCLI), users currently only see - // the wrapper "An error occurred while compiling the CSS files" with no useful detail. - Java java = createJava(org.apache.maven.doxia.logging.Log.LEVEL_INFO); + // The Java task is created via createJava() (overridden in this class to use INFO log + // level) so subprocess output -- including stack traces from CN1CSSCLI failures -- + // shows up in normal mvn output instead of being hidden at DEBUG. + Java java = createJava(); java.setDir(getCN1ProjectDir()); java.setJar(getDesignerJar()); java.setFork(true); From ba8044ef0ac49c06e2ea5e17db5ba7e7ca9e70e2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 2 May 2026 20:02:44 +0300 Subject: [PATCH 3/5] Fix CSS compiler StringIndexOutOfBoundsException for initializr projects The user shared the missing stack trace from issue #4850: at java.base/java.lang.String.substring(String.java:2899) at com.codename1.ui.plaf.UIManager.parseTextFieldInputMode(UIManager.java:2434) at com.codename1.ui.plaf.UIManager.setBundle(UIManager.java:2419) at com.codename1.impl.javase.JavaSEPort.enableAutoLocalizationBundle(...) at com.codename1.impl.javase.JavaSEPort.init(JavaSEPort.java:5598) at com.codename1.impl.CodenameOneImplementation.initImpl(...) at com.codename1.ui.Display.init(Display.java:351) at com.codename1.designer.css.CN1CSSCLI.main(CN1CSSCLI.java:713) This is not a path-related bug -- every initializr-generated project hits it at css-goal time. Three pieces interact: 1. JavaSEPort.findLocalizationDirectory auto-creates src/main/l10n if it is missing, and enableAutoLocalizationBundle installs an AutoLocalizationBundle for it. 2. AutoLocalizationBundle.get echoes any missing key back as its own value -- the simulator's "wormhole" so devs can spot untranslated strings. 3. UIManager.setBundle queries "@im" on every bundle install. With the echo behavior, "@im" -> "@im", which is then tokenized to ["@im"], "@im-@im" is queried (which echoes "@im-@im"), and parseTextFieldInputMode crashes on substring(0, indexOf('=')) because that token has no '=' (range [0, -1) of length 7). The CSS compiler subprocess inherits all of this because CN1CSSCLI.main calls Display.init -> JavaSEPort.init -> enableAutoLocalizationBundle. Fixes: - AutoLocalizationBundle.get returns null for keys starting with '@'. Meta-keys (@rtl, @im, @im-) are configuration entries that callers distinguish from "missing" by checking for null. Echoing the key back is semantically wrong AND broke setBundle. Real meta-key values that exist in the underlying file (e.g. @rtl=true in a Hebrew bundle) are still returned -- only fabrication is suppressed. - UIManager.parseTextFieldInputMode skips tokens without '=' and skips entries whose key isn't a valid integer. Defensive belt-and-suspenders so any bundle with malformed input-mode entries degrades gracefully instead of failing the whole bundle install. - New UIManagerSetBundleTest exercises setBundle against an echo-bundle (matches pre-fix AutoLocalizationBundle behavior) and against directly malformed @im/@im-X entries. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/ui/plaf/UIManager.java | 13 +++- .../com/codename1/impl/javase/JavaSEPort.java | 9 +++ .../ui/plaf/UIManagerSetBundleTest.java | 71 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index bf15bd88da..52a520c2a8 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -2431,9 +2431,20 @@ private Hashtable parseTextFieldInputMode(String s) { for (int iter = 0; iter < count; iter++) { String t = (String) tokens.elementAt(iter); int pos = t.indexOf('='); + // A malformed bundle (e.g. simulator AutoLocalizationBundle echoing the @im key + // back as its value) can produce tokens with no '='. Skip them rather than + // crashing on substring(0, -1). + if (pos < 0) { + continue; + } String key = t.substring(0, pos); String val = t.substring(pos + 1); - response.put(Integer.valueOf(key), val); + try { + response.put(Integer.valueOf(key), val); + } catch (NumberFormatException nfe) { + // Same defense: a non-numeric key means the entry isn't a real input-mode + // descriptor, so just skip it rather than failing the entire bundle install. + } } return response; } diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 404e329d76..72320f31e7 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -14295,6 +14295,15 @@ public synchronized String get(Object key) { if (key instanceof String) { String strKey = (String) key; if (value == null) { + // Don't auto-fabricate values for meta-keys like @rtl, @im, @im-. + // These are configuration entries that callers (e.g. UIManager.setBundle) + // distinguish from "missing" by checking for null. If we echo the key back + // as the value, setBundle will treat "@im" as a real input-mode descriptor, + // tokenize it, and crash inside parseTextFieldInputMode when the resulting + // token has no '='. + if (strKey.startsWith("@")) { + return null; + } String autoValue = strKey; putInternal(strKey, autoValue); storeEntry(strKey, autoValue, true); diff --git a/tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java b/tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java new file mode 100644 index 0000000000..2a8f88851e --- /dev/null +++ b/tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java @@ -0,0 +1,71 @@ +package com.codename1.ui.plaf; + +import com.codename1.testing.AbstractTest; + +import java.util.HashMap; +import java.util.Map; + +/** + * Regression test for the StringIndexOutOfBoundsException seen at CSS-compile + * time in initializr-generated projects (issue #4850). + * + * The simulator's AutoLocalizationBundle echoes any missing key back as its + * own value. setBundle queries `@im` (the input-mode descriptor) on every + * bundle install, and when the underlying bundle is empty the auto-bundle + * answered "@im". setBundle then tokenized that, queried "@im-@im", got + * "@im-@im" back, and parseTextFieldInputMode crashed on substring(0, -1). + * + * The defensive fix lives in UIManager.parseTextFieldInputMode (skip tokens + * without '=' and skip non-numeric keys). This test simulates an offending + * bundle and verifies setBundle no longer throws. + */ +public class UIManagerSetBundleTest extends AbstractTest { + + /** + * Bundle that echoes any missing key back as its value -- matches the + * pre-fix AutoLocalizationBundle behavior so we can exercise setBundle's + * defensive path independently of the simulator. + */ + private static final class EchoBundle extends HashMap { + @Override + public String get(Object key) { + String value = super.get(key); + if (value != null) { + return value; + } + if (key instanceof String) { + return (String) key; + } + return null; + } + } + + @Override + public boolean runTest() throws Exception { + UIManager mgr = UIManager.getInstance(); + + Map echo = new EchoBundle(); + // No assertion needed -- before the fix this throws StringIndexOutOfBoundsException + // inside parseTextFieldInputMode("@im-@im") because the token has no '='. + mgr.setBundle(echo); + + // Same flow but with a partially-populated bundle that has "@im" pointing at a + // malformed input-mode descriptor (no '='). Should also not crash. + Map malformed = new HashMap(); + malformed.put("@im", "ABC"); + malformed.put("@im-ABC", "garbage_with_no_equals"); + mgr.setBundle(malformed); + + // And one with a non-numeric key in the input-mode descriptor -- previously this + // would have thrown NumberFormatException out of parseTextFieldInputMode. + Map nonNumericKey = new HashMap(); + nonNumericKey.put("@im", "ABC"); + nonNumericKey.put("@im-ABC", "notANumber=value"); + mgr.setBundle(nonNumericKey); + + // Restore default bundle so we don't bleed state into other tests. + mgr.setBundle(null); + + return true; + } +} From 2d1e1f83c133f1fcac3635514f872258e947cefa Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 2 May 2026 20:05:43 +0300 Subject: [PATCH 4/5] Revert parseTextFieldInputMode fail-safe; keep AutoLocalizationBundle fix Per review: silently skipping malformed `@im` tokens hides legitimate bugs in user-supplied bundles. Real malformed input should fail loudly, not be swallowed. The actual root cause -- AutoLocalizationBundle fabricating values for @-prefixed meta-keys -- stays fixed. That's the surgical change: the auto-localize wormhole was returning fake values for keys that callers explicitly use null/non-null to gate features (@im, @rtl, @im-), which is semantically wrong and broke setBundle. Moves the regression coverage from the no-op stub in core to the existing AutoLocalizationBundleTest in the JavaSE port (where the bundle class actually lives), asserting: - @-prefixed meta-keys are NOT auto-fabricated - @-prefixed meta-keys that exist in the underlying file ARE returned Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/ui/plaf/UIManager.java | 13 +--- .../ui/plaf/UIManagerSetBundleTest.java | 71 ------------------- .../javase/AutoLocalizationBundleTest.java | 15 ++++ 3 files changed, 16 insertions(+), 83 deletions(-) delete mode 100644 tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index 52a520c2a8..bf15bd88da 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -2431,20 +2431,9 @@ private Hashtable parseTextFieldInputMode(String s) { for (int iter = 0; iter < count; iter++) { String t = (String) tokens.elementAt(iter); int pos = t.indexOf('='); - // A malformed bundle (e.g. simulator AutoLocalizationBundle echoing the @im key - // back as its value) can produce tokens with no '='. Skip them rather than - // crashing on substring(0, -1). - if (pos < 0) { - continue; - } String key = t.substring(0, pos); String val = t.substring(pos + 1); - try { - response.put(Integer.valueOf(key), val); - } catch (NumberFormatException nfe) { - // Same defense: a non-numeric key means the entry isn't a real input-mode - // descriptor, so just skip it rather than failing the entire bundle install. - } + response.put(Integer.valueOf(key), val); } return response; } diff --git a/tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java b/tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java deleted file mode 100644 index 2a8f88851e..0000000000 --- a/tests/core/src/com/codename1/ui/plaf/UIManagerSetBundleTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.codename1.ui.plaf; - -import com.codename1.testing.AbstractTest; - -import java.util.HashMap; -import java.util.Map; - -/** - * Regression test for the StringIndexOutOfBoundsException seen at CSS-compile - * time in initializr-generated projects (issue #4850). - * - * The simulator's AutoLocalizationBundle echoes any missing key back as its - * own value. setBundle queries `@im` (the input-mode descriptor) on every - * bundle install, and when the underlying bundle is empty the auto-bundle - * answered "@im". setBundle then tokenized that, queried "@im-@im", got - * "@im-@im" back, and parseTextFieldInputMode crashed on substring(0, -1). - * - * The defensive fix lives in UIManager.parseTextFieldInputMode (skip tokens - * without '=' and skip non-numeric keys). This test simulates an offending - * bundle and verifies setBundle no longer throws. - */ -public class UIManagerSetBundleTest extends AbstractTest { - - /** - * Bundle that echoes any missing key back as its value -- matches the - * pre-fix AutoLocalizationBundle behavior so we can exercise setBundle's - * defensive path independently of the simulator. - */ - private static final class EchoBundle extends HashMap { - @Override - public String get(Object key) { - String value = super.get(key); - if (value != null) { - return value; - } - if (key instanceof String) { - return (String) key; - } - return null; - } - } - - @Override - public boolean runTest() throws Exception { - UIManager mgr = UIManager.getInstance(); - - Map echo = new EchoBundle(); - // No assertion needed -- before the fix this throws StringIndexOutOfBoundsException - // inside parseTextFieldInputMode("@im-@im") because the token has no '='. - mgr.setBundle(echo); - - // Same flow but with a partially-populated bundle that has "@im" pointing at a - // malformed input-mode descriptor (no '='). Should also not crash. - Map malformed = new HashMap(); - malformed.put("@im", "ABC"); - malformed.put("@im-ABC", "garbage_with_no_equals"); - mgr.setBundle(malformed); - - // And one with a non-numeric key in the input-mode descriptor -- previously this - // would have thrown NumberFormatException out of parseTextFieldInputMode. - Map nonNumericKey = new HashMap(); - nonNumericKey.put("@im", "ABC"); - nonNumericKey.put("@im-ABC", "notANumber=value"); - mgr.setBundle(nonNumericKey); - - // Restore default bundle so we don't bleed state into other tests. - mgr.setBundle(null); - - return true; - } -} diff --git a/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java b/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java index d25aeb30ee..4d9db36610 100644 --- a/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java +++ b/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java @@ -62,6 +62,21 @@ public boolean runTest() throws Exception { Map bundleReloadedMap = (Map) bundleReloaded; assertEqual("missingKey", bundleReloadedMap.get("missingKey"), "Existing persisted values should be loaded"); + // Regression: meta-keys (anything starting with `@`) must NOT be auto-fabricated. + // The auto-fabrication was breaking UIManager.setBundle, which queries `@im` / + // `@rtl` on every install and uses null-vs-non-null to mean "feature disabled". + // When the bundle echoed "@im" -> "@im", setBundle tokenized it, queried + // "@im-@im", got "@im-@im" back, and parseTextFieldInputMode crashed on + // substring(0, indexOf('=')) for a token with no '=' (issue #4850). + assertNull(bundleReloadedMap.get("@im"), "@-prefixed meta-keys must not be auto-fabricated"); + assertNull(bundleReloadedMap.get("@rtl"), "@-prefixed meta-keys must not be auto-fabricated"); + assertNull(bundleReloadedMap.get("@im-FOO"), "@-prefixed meta-keys must not be auto-fabricated"); + + // But real meta-key values that exist in the underlying file are still returned. + // Stage one by writing it through the explicit put path (which persists to disk). + bundleReloadedMap.put("@rtl", "true"); + assertEqual("true", bundleReloadedMap.get("@rtl"), "Existing meta-key values should still be returned"); + return true; } finally { deleteRecursive(tempDir); From 473178c68d027b65d0bdfcc93dc9393aff0610e0 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 2 May 2026 20:14:38 +0300 Subject: [PATCH 5/5] Initializr: ship Bundle.properties stub with @im= to dodge simulator wormhole Workaround for the AutoLocalizationBundle @im fabrication crash in shipped Codename One <= 7.0.236. The proper fix lives in JavaSEPort (don't fabricate values for `@`-prefixed meta-keys) and is on this branch, but it requires a new framework release. Until then, every initializr-generated project crashes at css-goal time inside the CSS compiler subprocess (CN1CSSCLI -> Display.init -> JavaSEPort.init -> enableAutoLocalizationBundle -> UIManager.setBundle -> parseTextFieldInputMode on substring(0, -1) for "@im-@im"). Ship `common/src/main/l10n/Bundle.properties` with a single `@im=` entry on every generated project. Two reasons it works: 1. JavaSEPort.findDefaultLocalizationBundleFile prefers Bundle.properties over any other file in src/main/l10n, so the AutoLocalizationBundle loads our stub as its base. 2. With `@im=""` already in the bundle's underlying Hashtable, AutoLocalizationBundle.get("@im") returns "" instead of fabricating "@im". setBundle sees length 0 and skips the input-mode block, so parseTextFieldInputMode is never called. The stub is unconditional (added to every project, with or without localization bundles enabled) because the bug fires regardless -- enableAutoLocalizationBundle auto-creates src/main/l10n in the CSS compiler subprocess even on projects that didn't request localization. The matrix test asserts the stub is present on every generated project combination so this workaround can't silently regress. Once the AutoLocalizationBundle fix lands in a release and the initializr is bumped past it, this stub can be removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../initializr/model/GeneratorModel.java | 38 +++++++++++++++++++ .../model/GeneratorModelMatrixTest.java | 10 +++++ 2 files changed, 48 insertions(+) diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index b8755b5b01..1c6a09fc1f 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -90,6 +90,7 @@ void writeProjectZip(OutputStream outputStream) throws IOException { copyZipEntriesToMap(template.CSS, mergedEntries, ZipEntryType.TEMPLATE_CSS); copyZipEntriesToMap(template.SOURCE_ZIP, mergedEntries, ZipEntryType.TEMPLATE_SOURCE); addLocalizationEntries(mergedEntries); + addAutoLocalizationBundleStub(mergedEntries); try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { for (Map.Entry fileEntry : mergedEntries.entrySet()) { @@ -102,6 +103,43 @@ void writeProjectZip(OutputStream outputStream) throws IOException { } + /** + * Workaround for a bug in shipped Codename One versions (<= 7.0.236) where the + * simulator's AutoLocalizationBundle echoes any missing key back as its own value. + * UIManager.setBundle queries `@im` on every bundle install, gets `"@im"` back from + * the wormhole, tokenizes it, queries `"@im-@im"`, gets `"@im-@im"` back, then + * crashes inside parseTextFieldInputMode on substring(0, indexOf('=')) for a token + * with no `=`. The CSS compiler subprocess (CN1CSSCLI -> Display.init -> JavaSEPort.init + * -> enableAutoLocalizationBundle) hits this on every initializr-generated project. + * + * The proper fix lives in JavaSEPort.AutoLocalizationBundle (don't fabricate values + * for `@`-prefixed meta-keys), but that requires a new framework release. As a + * workaround we ship an empty `Bundle.properties` with `@im=`, which: + * 1. Is preferred by JavaSEPort.findDefaultLocalizationBundleFile over any other + * bundle file in src/main/l10n, so the AutoLocalizationBundle loads it as base. + * 2. Pre-populates `@im=""` in the bundle's underlying Hashtable, so + * AutoLocalizationBundle.get("@im") returns "" (not the fabricated "@im"), + * which has length 0, so setBundle skips the input-mode block entirely. + * + * This is unconditional (added to every generated project) because + * enableAutoLocalizationBundle auto-creates `src/main/l10n` even when the user + * didn't ask for localization bundles, so the crash hits projects without any + * localization too. Remove this stub once the framework fix has shipped and + * cn1.plugin.version is bumped past it. + */ + private void addAutoLocalizationBundleStub(Map mergedEntries) throws IOException { + String stub = "# Workaround for the simulator AutoLocalizationBundle @im fabrication crash\n" + + "# in Codename One <= 7.0.236. Once the framework fix ships, this file can be removed.\n" + + "# See GeneratorModel.addAutoLocalizationBundleStub for the full story.\n" + + "@im=\n"; + copySingleTextEntryToMap( + "common/src/main/l10n/Bundle.properties", + stub, + mergedEntries, + ZipEntryType.COMMON + ); + } + private void addLocalizationEntries(Map mergedEntries) throws IOException { if (!isBareTemplate() || !options.includeLocalizationBundles) { return; diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index 2b7904a021..67c20a31ec 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -194,9 +194,19 @@ private void validateCombination(Template template, IDE ide) throws Exception { assertMainSourceFile(entries, template, packageName, mainClassName, false); assertThemeDefaults(entries, template); assertLocalizationBundles(entries, template, false); + assertAutoLocalizationBundleStub(entries); assertNoTemplatePlaceholders(entries, template); } + private void assertAutoLocalizationBundleStub(Map entries) { + // Workaround stub for the simulator AutoLocalizationBundle @im fabrication crash + // in shipped CN1 <= 7.0.236. Must be present on every generated project (with or + // without localization bundles) because enableAutoLocalizationBundle auto-creates + // src/main/l10n in the CSS compiler subprocess and hits the crash regardless. + String stub = getText(entries, "common/src/main/l10n/Bundle.properties"); + assertContains(stub, "@im=", "Generated project must ship Bundle.properties with @im= to suppress simulator wormhole crash"); + } + private void assertThemeDefaults(Map entries, Template template) { if (template != Template.BAREBONES && template != Template.KOTLIN) { return;