diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java index 8092772f86..87133bcc8a 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java @@ -50,6 +50,7 @@ public void runApp() { final boolean[] roundedButtons = new boolean[]{true}; final boolean[] includeLocalizationBundles = new boolean[]{true}; final ProjectOptions.PreviewLanguage[] previewLanguage = new ProjectOptions.PreviewLanguage[]{ProjectOptions.PreviewLanguage.ENGLISH}; + final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_8}; final RadioButton[] templateButtons = new RadioButton[Template.values().length]; final SpanLabel summaryLabel = new SpanLabel(); final TemplatePreviewPanel previewPanel = new TemplatePreviewPanel(selectedTemplate[0]); @@ -69,7 +70,7 @@ public void runApp() { public void run() { ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], - includeLocalizationBundles[0], previewLanguage[0] + includeLocalizationBundles[0], previewLanguage[0], javaVersion[0] ); previewPanel.setTemplate(selectedTemplate[0]); previewPanel.setOptions(options); @@ -99,6 +100,7 @@ public void run() { final Container idePanel = createIdeSelectorPanel(selectedIde, refresh); final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, refresh); final Container localizationPanel = createLocalizationPanel(includeLocalizationBundles, previewLanguage, refresh, previewPanel); + final Container javaPanel = createJavaOptionsPanel(javaVersion, refresh); themePanelRef[0] = themePanel; final Container settingsPanel = BoxLayout.encloseY(summaryLabel); @@ -106,6 +108,7 @@ public void run() { advancedAccordion.addContent("IDE", idePanel); advancedAccordion.addContent("Theme Customization", themePanel); advancedAccordion.addContent("Localization", localizationPanel); + advancedAccordion.addContent("Java Version", javaPanel); advancedAccordion.addContent("Current Settings", settingsPanel); advancedAccordion.setAutoClose(false); advancedAccordion.setScrollable(false); @@ -133,7 +136,7 @@ public void run() { String packageName = packageField.getText() == null ? "" : packageField.getText().trim(); ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], - includeLocalizationBundles[0], previewLanguage[0] + includeLocalizationBundles[0], previewLanguage[0], javaVersion[0] ); GeneratorModel.create(selectedIde[0], selectedTemplate[0], appName, packageName, options).generate(); }); @@ -314,6 +317,31 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe ); } + + private Container createJavaOptionsPanel(ProjectOptions.JavaVersion[] javaVersion, Runnable onSelectionChanged) { + Container selector = new Container(new GridLayout(2, 1)); + selector.setUIID("InitializrChoicesGrid"); + ButtonGroup group = new ButtonGroup(); + + for (ProjectOptions.JavaVersion version : ProjectOptions.JavaVersion.values()) { + RadioButton button = new RadioButton(version.label); + button.setToggle(true); + button.setUIID("InitializrChoice"); + group.add(button); + selector.add(button); + if (version == javaVersion[0]) { + button.setSelected(true); + } + button.addActionListener(evt -> { + if (button.isSelected()) { + javaVersion[0] = version; + onSelectionChanged.run(); + } + }); + } + return selector; + } + private Container createTemplateSelector(Template[] selectedTemplate, RadioButton[] templateButtons, Runnable onSelectionChanged) { Container selector = new Container(new GridLayout(2, 2)); selector.setUIID("InitializrChoicesGrid"); @@ -509,6 +537,7 @@ private String createSummary(String appName, String packageName, Template templa + "Rounded Buttons: " + (options.roundedButtons ? "Yes" : "No") + "\n" + "Localization Bundles: " + (options.includeLocalizationBundles ? "Yes" : "No") + "\n" + "Preview Language: " + options.previewLanguage.label + "\n" + + "Java: " + options.javaVersion.label + "\n" + "Kotlin: " + (template.IS_KOTLIN ? "Yes" : "No"); } 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 e555b41095..e0cf3fa54d 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 @@ -179,6 +179,7 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw content = StringUtil.replaceAll(content, "myappname", appName.toLowerCase()); if ("common/codenameone_settings.properties".equals(targetPath)) { content = replaceProperty(content, "codename1.kotlin", String.valueOf(template.IS_KOTLIN)); + content = applyJavaVersionSettings(content); } if (options.includeLocalizationBundles && isBareTemplate()) { content = injectLocalizationBootstrap(targetPath, content); @@ -186,6 +187,9 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw if (isBareTemplate() && "common/src/main/css/theme.css".equals(targetPath)) { content += buildThemeOverrides(); } + if ("common/pom.xml".equals(targetPath)) { + content = applyJavaVersionToPom(content); + } if ("pom.xml".equals(targetPath)) { content = replaceTagValue(content, "cn1.plugin.version", CN1_PLUGIN_VERSION); } @@ -196,6 +200,23 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw } + + private String applyJavaVersionSettings(String content) { + if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL) { + content = replaceProperty(content, "codename1.arg.java.version", "17"); + } + return content; + } + + private String applyJavaVersionToPom(String content) { + if (options.javaVersion != ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL) { + return content; + } + content = StringUtil.replaceAll(content, "1.8", "17"); + content = StringUtil.replaceAll(content, "1.8", "17"); + return content; + } + private String injectLocalizationBootstrap(String targetPath, String content) { String javaMainPath = "common/src/main/java/" + packageName.replace('.', '/') + "/" + appName + ".java"; String kotlinMainPath = "common/src/main/kotlin/" + packageName.replace('.', '/') + "/" + appName + ".kt"; diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java index 7595fa1fc3..d33361ec9d 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java @@ -43,22 +43,41 @@ public enum Accent { ORANGE } + public enum JavaVersion { + JAVA_8("Java 8"), + JAVA_17_EXPERIMENTAL("Java 17 (Experimental)"); + + public final String label; + + JavaVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + public final ThemeMode themeMode; public final Accent accent; public final boolean roundedButtons; public final boolean includeLocalizationBundles; public final PreviewLanguage previewLanguage; + public final JavaVersion javaVersion; public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons, - boolean includeLocalizationBundles, PreviewLanguage previewLanguage) { + boolean includeLocalizationBundles, PreviewLanguage previewLanguage, + JavaVersion javaVersion) { this.themeMode = themeMode; this.accent = accent; this.roundedButtons = roundedButtons; this.includeLocalizationBundles = includeLocalizationBundles; this.previewLanguage = previewLanguage == null ? PreviewLanguage.ENGLISH : previewLanguage; + this.javaVersion = javaVersion == null ? JavaVersion.JAVA_8 : javaVersion; } public static ProjectOptions defaults() { - return new ProjectOptions(ThemeMode.LIGHT, Accent.DEFAULT, true, true, PreviewLanguage.ENGLISH); + return new ProjectOptions(ThemeMode.LIGHT, Accent.DEFAULT, true, true, PreviewLanguage.ENGLISH, JavaVersion.JAVA_8); } } 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 new file mode 100644 index 0000000000..5779d205f6 --- /dev/null +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelIntegrationBuildTest.java @@ -0,0 +1,233 @@ +package com.codename1.initializr.model; + +import com.codename1.io.Util; +import com.codename1.testing.AbstractTest; +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.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +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.Locale; +import java.util.Map; + +/** + * Integration-oriented test that generates real projects and attempts a Maven compile + * using selected JDK homes. + */ +public class GeneratorModelIntegrationBuildTest extends AbstractTest { + @Override + public boolean runTest() throws Exception { + Path java8Or11 = findJava8Or11Home(); + Path java17 = findJavaHomeForMajor(17); + + if (java8Or11 == null) { + System.out.println("[WARN] Skipping Java 8/11 integration build check. No JDK 8 or 11 found."); + } else { + buildGeneratedProject(ProjectOptions.JavaVersion.JAVA_8, java8Or11, "java8-or-11"); + } + + if (java17 == null) { + System.out.println("[WARN] Skipping Java 17 integration build check. No JDK 17 found."); + } else { + buildGeneratedProject(ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL, java17, "java17"); + } + + return true; + } + + private void buildGeneratedProject(ProjectOptions.JavaVersion version, Path javaHome, String suffix) throws Exception { + String appName = "Integration" + suffix.replace("-", "") + "App"; + String packageName = "com.acme.initializr." + suffix.replace("-", ""); + + ProjectOptions options = new ProjectOptions( + ProjectOptions.ThemeMode.LIGHT, + ProjectOptions.Accent.DEFAULT, + true, + true, + ProjectOptions.PreviewLanguage.ENGLISH, + version + ); + + byte[] zip = createProjectZip(options, appName, packageName); + Path projectDir = Files.createTempDirectory("initializr-integration-" + suffix + "-"); + Path homeDir = Files.createTempDirectory("initializr-home-" + suffix + "-"); + ensureCodenameOneHome(homeDir); + unzipProject(zip, projectDir); + + int exitCode = runMavenCompile(projectDir, homeDir, javaHome); + assertTrue(exitCode == 0, "Generated project should compile with selected JDK. Version=" + version.label + " | exitCode=" + exitCode); + } + + private byte[] createProjectZip(ProjectOptions options, String appName, String packageName) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + GeneratorModel.create(IDE.INTELLIJ, Template.BAREBONES, appName, packageName, options).writeProjectZip(output); + return output.toByteArray(); + } + + private int runMavenCompile(Path projectDir, Path homeDir, Path javaHome) throws Exception { + ProcessBuilder pb = new ProcessBuilder( + "mvn", + "-f", "common/pom.xml", + "-DskipTests=true", + "-Dcodename1.platform=javase", + "-Duser.home=" + homeDir.toString(), + "compile" + ); + pb.directory(projectDir.toFile()); + pb.redirectErrorStream(true); + + Map env = pb.environment(); + env.put("JAVA_HOME", javaHome.toString()); + env.put("PATH", javaHome.resolve("bin") + File.pathSeparator + env.get("PATH")); + + List output = new ArrayList(); + Process process = pb.start(); + try (InputStream in = process.getInputStream()) { + java.io.BufferedReader r = new java.io.BufferedReader(new java.io.InputStreamReader(in)); + String line; + while ((line = r.readLine()) != null) { + output.add(line); + } + } + int exit = process.waitFor(); + if (exit != 0) { + StringBuilder sb = new StringBuilder(); + for (String line : output) { + if (sb.length() > 12000) { + sb.append("\n...[truncated]"); + break; + } + sb.append(line).append('\n'); + } + System.out.println(sb.toString()); + } + return exit; + } + + private void ensureCodenameOneHome(Path homeDir) throws IOException { + Path cn1Dir = homeDir.resolve(".codenameone"); + Files.createDirectories(cn1Dir); + Files.write(cn1Dir.resolve("guibuilder.jar"), new byte[0]); + Files.write(homeDir.resolve("CodeNameOneBuildClient.jar"), new byte[0]); + } + + private void unzipProject(byte[] zipData, Path destination) throws IOException { + ByteArrayInputStream input = new ByteArrayInputStream(zipData); + ZipInputStream zis = new ZipInputStream(input); + try { + ZipEntry entry = zis.getNextEntry(); + while (entry != null) { + if (!entry.isDirectory()) { + Path target = destination.resolve(entry.getName()); + Path parent = target.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + FileOutputStream fos = new FileOutputStream(target.toFile()); + try { + Util.copyNoClose(zis, fos, 8192); + } finally { + fos.close(); + } + } + zis.closeEntry(); + entry = zis.getNextEntry(); + } + } finally { + zis.close(); + input.close(); + } + } + + private Path findJava8Or11Home() throws Exception { + Path java11 = findJavaHomeForMajor(11); + if (java11 != null) { + return java11; + } + return findJavaHomeForMajor(8); + } + + private Path findJavaHomeForMajor(int major) throws Exception { + String envName = "INITIALIZR_JDK" + major + "_HOME"; + String envValue = System.getenv(envName); + if (envValue != null && envValue.length() > 0) { + Path candidate = Paths.get(envValue); + if (looksLikeJdkHome(candidate) && javaMajor(candidate) == major) { + return candidate; + } + } + + List candidates = new ArrayList(); + String currentJavaHome = System.getProperty("java.home"); + if (currentJavaHome != null) { + candidates.add(Paths.get(currentJavaHome).getParent()); + candidates.add(Paths.get(currentJavaHome)); + } + collectJvmCandidates(candidates, "/usr/lib/jvm"); + collectJvmCandidates(candidates, "/Library/Java/JavaVirtualMachines"); + collectJvmCandidates(candidates, "C:\\Program Files\\Java"); + + for (Path candidate : candidates) { + if (!looksLikeJdkHome(candidate)) { + continue; + } + if (javaMajor(candidate) == major) { + return candidate; + } + Path nestedHome = candidate.resolve("Contents/Home"); + if (looksLikeJdkHome(nestedHome) && javaMajor(nestedHome) == major) { + return nestedHome; + } + } + + return null; + } + + private void collectJvmCandidates(List out, String directory) throws IOException { + Path root = Paths.get(directory); + if (!Files.isDirectory(root)) { + return; + } + out.add(root); + try (java.util.stream.Stream stream = Files.list(root)) { + stream.forEach(out::add); + } + } + + private boolean looksLikeJdkHome(Path candidate) { + return candidate != null && Files.isRegularFile(candidate.resolve("bin").resolve("java")); + } + + private int javaMajor(Path javaHome) throws Exception { + ProcessBuilder pb = new ProcessBuilder(javaHome.resolve("bin").resolve("java").toString(), "-version"); + pb.redirectErrorStream(true); + Process process = pb.start(); + StringBuilder out = new StringBuilder(); + try (InputStream in = process.getInputStream()) { + int b; + while ((b = in.read()) != -1) { + out.append((char) b); + } + } + process.waitFor(); + + String text = out.toString().toLowerCase(Locale.ROOT); + if (text.indexOf(" version \"1.8") >= 0) { + return 8; + } + java.util.regex.Matcher m = java.util.regex.Pattern.compile("version \\\"([0-9]+)").matcher(text); + if (m.find()) { + return Integer.parseInt(m.group(1)); + } + return -1; + } +} 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 df6d831423..fe4c56d105 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 @@ -21,9 +21,30 @@ public boolean runTest() throws Exception { validateCombination(template, ide); } } + validateExperimentalJava17Generation(); return true; } + + private void validateExperimentalJava17Generation() throws Exception { + String mainClassName = "DemoExperimentalJava17"; + String packageName = "com.acme.experimental.java17"; + ProjectOptions options = new ProjectOptions( + ProjectOptions.ThemeMode.LIGHT, + ProjectOptions.Accent.DEFAULT, + true, + true, + ProjectOptions.PreviewLanguage.ENGLISH, + ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL + ); + + byte[] zipData = createProjectZip(IDE.INTELLIJ, Template.BAREBONES, mainClassName, packageName, options); + Map entries = readZipEntries(zipData); + + assertCommonPom(entries, Template.BAREBONES, packageName, mainClassName, true); + assertSettings(entries, Template.BAREBONES, packageName, mainClassName, true); + } + private void validateCombination(Template template, IDE ide) throws Exception { String mainClassName = "Demo" + template.ordinal() + ide.ordinal() + "App"; String packageName = "com.acme.t" + template.ordinal() + ".i" + ide.ordinal(); @@ -34,8 +55,8 @@ private void validateCombination(Template template, IDE ide) throws Exception { assertIdeFiles(ide, entries, mainClassName); assertGitIgnore(entries); assertRootPom(entries, packageName, mainClassName); - assertCommonPom(entries, template, packageName, mainClassName); - assertSettings(entries, template, packageName, mainClassName); + assertCommonPom(entries, template, packageName, mainClassName, false); + assertSettings(entries, template, packageName, mainClassName, false); assertMainSourceFile(entries, template, packageName, mainClassName); assertLocalizationBundles(entries, template); assertNoTemplatePlaceholders(entries, template); @@ -47,6 +68,12 @@ private static byte[] createProjectZip(IDE ide, Template template, String appNam return output.toByteArray(); } + private static byte[] createProjectZip(IDE ide, Template template, String appName, String packageName, ProjectOptions options) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + GeneratorModel.create(ide, template, appName, packageName, options).writeProjectZip(output); + return output.toByteArray(); + } + private static Map readZipEntries(byte[] zipData) throws IOException { Map entries = new HashMap(); ByteArrayInputStream input = new ByteArrayInputStream(zipData); @@ -97,13 +124,20 @@ private void assertRootPom(Map entries, String packageName, Stri assertFalse(pom.indexOf("myappname") >= 0, "Root pom still contains placeholder app name"); } - private void assertCommonPom(Map entries, Template template, String packageName, String mainClassName) { + private void assertCommonPom(Map entries, Template template, String packageName, String mainClassName, boolean expectJava17) { String pom = getText(entries, "common/pom.xml"); assertContains(pom, packageName, "Common pom should include package"); assertContains(pom, mainClassName.toLowerCase(), "Common pom should include app artifact"); assertContains(pom, "codenameone-javase", "Common pom should include codenameone-javase test dependency"); assertContains(pom, "serializer", "Common pom should include xalan serializer for CN1 generate-gui-sources"); assertContains(pom, "2.7.3", "Common pom should pin serializer version expected by CN1 plugin classpath"); + if (expectJava17) { + assertContains(pom, "17", "Common pom should use Java 17 source when selected"); + assertContains(pom, "17", "Common pom should use Java 17 target when selected"); + } else { + assertContains(pom, "1.8", "Common pom should default to Java 8 source"); + assertContains(pom, "1.8", "Common pom should default to Java 8 target"); + } if (template == Template.GRUB) { assertContains(pom, "" + mainClassName.toLowerCase() + "-CodeRAD", "Grub common pom should include local CodeRAD cn1lib dependency"); assertContains(pom, "1.0-SNAPSHOT", "Grub common pom should use local snapshot CodeRAD cn1lib"); @@ -117,12 +151,17 @@ private void assertCommonPom(Map entries, Template template, Str assertFalse(pom.indexOf("myappname") >= 0, "Common pom still contains placeholder app name"); } - private void assertSettings(Map entries, Template template, String packageName, String mainClassName) { + private void assertSettings(Map entries, Template template, String packageName, String mainClassName, boolean expectJava17) { String settings = getText(entries, "common/codenameone_settings.properties"); assertContains(settings, "codename1.packageName=" + packageName, "Settings should include requested package"); assertContains(settings, "codename1.mainName=" + mainClassName, "Settings should include requested main class"); assertContains(settings, "codename1.displayName=" + mainClassName, "Settings should include requested display name"); assertContains(settings, "codename1.kotlin=" + String.valueOf(template.IS_KOTLIN), "Settings should include template kotlin flag"); + if (expectJava17) { + assertContains(settings, "codename1.arg.java.version=17", "Settings should include Java 17 version when selected"); + } else { + assertFalse(settings.indexOf("codename1.arg.java.version=17") >= 0, "Settings should not force Java 17 by default"); + } } private void assertMainSourceFile(Map entries, Template template, String packageName, String mainClassName) {