From f7e2c61cbbeea365307b2833f015916ea99b15da Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 26 May 2026 04:38:08 +0300 Subject: [PATCH] Generate the JavaSE desktop stub from codenameone_settings.properties Initializr-generated projects ship only the Lifecycle subclass and no Stub.java, so `mvn verify -P run-desktop` failed with ClassNotFoundException. The stub carries app-specific configuration (title, dimensions, fullscreen flag, etc.) and should be emitted by the plugin from settings/build-hints rather than shipped statically, the way the iOS and Android builders already do. GenerateDesktopAppWrapperMojo now writes target/generated-sources/cn1-desktop//Stub.java from a packaged template, substituting codename1.mainName / packageName / displayName / version and the codename1.arg.desktop.* build hints (title, width, height, fullscreen, resizable, adaptToRetina), and adds the directory as a compile source root. A hand-rolled src/desktop/java//Stub.java still wins so apps can take full control when the hint surface is not enough. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../maven/GenerateDesktopAppWrapperMojo.java | 148 ++++++++++++- .../maven/desktop-app-stub-template.java | 201 ++++++++++++++++++ 2 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 maven/codenameone-maven-plugin/src/main/resources/com/codename1/maven/desktop-app-stub-template.java diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateDesktopAppWrapperMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateDesktopAppWrapperMojo.java index a86539da83..aaca16ecbd 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateDesktopAppWrapperMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateDesktopAppWrapperMojo.java @@ -1,5 +1,6 @@ package com.codename1.maven; +import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -7,28 +8,55 @@ import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import static com.codename1.maven.PathUtil.path; /** - * Generates The SEWrapper, or the main class for a JavaSE desktop app. This is used by the javase module - * of the cn1app-archetype. + * Generates the desktop SE wrapper (the {@code Stub} class containing + * the {@code main(String[])} that boots the Swing JFrame and instantiates the + * Codename One {@code Lifecycle} subclass). This Mojo is the JavaSE analogue of + * the stub generation that {@link com.codename1.builders.AndroidGradleBuilder} + * and {@link com.codename1.builders.IPhoneBuilder} do for their respective + * targets - taking values from {@code codenameone_settings.properties} (and + * {@code codename1.arg.desktop.*} build hints) and emitting the platform's + * entry-point class so the app-level project does not have to ship a + * hand-maintained stub. + * + *

If the developer drops {@code javase/src/desktop/java//Stub.java} + * by hand it wins: this Mojo will skip generation, and the existing source-root + * registration below picks it up. That makes the static file a complete + * override - useful when an app needs custom Swing setup that's outside what + * the build-hint surface covers. */ @Mojo(name="generate-desktop-app-wrapper") public class GenerateDesktopAppWrapperMojo extends AbstractCN1Mojo { + private static final String GENERATED_SOURCES_DIR = "cn1-desktop"; + @Override protected void executeImpl() throws MojoExecutionException, MojoFailureException { + generateIcons(); + generateStub(); + registerCustomStubSourceRoot(); + } + + private void generateIcons() throws MojoExecutionException { String iconPath = properties.getProperty("codename1.icon"); + if (iconPath == null) { + getLog().warn("codename1.icon not set in codenameone_settings.properties. Skipping desktop app icon generation."); + return; + } File iconFile = new File(iconPath); if (!iconFile.isAbsolute()) { iconFile = new File(getCN1ProjectDir(), iconFile.getPath()); } if (!iconFile.exists()) { - getLog().warn("Icon file "+iconFile+" not found. Skipping desktop appp icon generation."); + getLog().warn("Icon file "+iconFile+" not found. Skipping desktop app icon generation."); return; } File outputDir = new File(project.getBuild().getOutputDirectory()); @@ -46,17 +74,127 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException } catch (IOException ex) { throw new MojoExecutionException("Failed to generate icons", ex); } + } - // Now get the wrapper + private void registerCustomStubSourceRoot() { File wrapperSources = new File(project.getBasedir(), path("src", "desktop", "java")); if (wrapperSources.exists()) { project.addCompileSourceRoot(wrapperSources.getAbsolutePath()); } + } + + private void generateStub() throws MojoExecutionException { + String packageName = properties.getProperty("codename1.packageName"); + String mainName = properties.getProperty("codename1.mainName"); + if (packageName == null || mainName == null) { + getLog().warn("codename1.packageName or codename1.mainName not set in codenameone_settings.properties. Skipping desktop stub generation."); + return; + } + + // If the developer hand-rolls a stub under src/desktop/java//Stub.java + // treat it as a full override and skip generation - the source root that + // registerCustomStubSourceRoot adds will pick it up. + String packagePath = packageName.replace('.', File.separatorChar); + File customStub = new File(project.getBasedir(), + path("src", "desktop", "java", packagePath, mainName + "Stub.java")); + if (customStub.exists()) { + getLog().info("Custom desktop stub found at " + customStub.getAbsolutePath() + " - skipping generation."); + return; + } + + File generatedRoot = new File(project.getBuild().getDirectory(), + path("generated-sources", GENERATED_SOURCES_DIR)); + File generatedPkgDir = new File(generatedRoot, packagePath); + generatedPkgDir.mkdirs(); + File generatedStub = new File(generatedPkgDir, mainName + "Stub.java"); + + String template; + try (InputStream in = getClass().getResourceAsStream("desktop-app-stub-template.java")) { + if (in == null) { + throw new MojoExecutionException("Could not load desktop-app-stub-template.java resource from plugin jar."); + } + template = IOUtils.toString(in, StandardCharsets.UTF_8); + } catch (IOException ex) { + throw new MojoExecutionException("Failed to read desktop stub template", ex); + } + + String stubSource = applyTemplate(template, packageName, mainName); + + try { + Files.write(generatedStub.toPath(), stubSource.getBytes(StandardCharsets.UTF_8)); + getLog().info("Generated desktop stub: " + generatedStub.getAbsolutePath()); + } catch (IOException ex) { + throw new MojoExecutionException("Failed to write generated desktop stub " + generatedStub, ex); + } + + project.addCompileSourceRoot(generatedRoot.getAbsolutePath()); + } + private String applyTemplate(String template, String packageName, String mainName) { + // Default APP_TITLE to displayName; fall back to mainName so we never inject an empty literal. + String displayName = properties.getProperty("codename1.displayName", mainName); + String appTitle = arg("desktop.title", displayName); + String appVersion = properties.getProperty("codename1.version", "1.0"); + + String width = arg("desktop.width", "800"); + String height = arg("desktop.height", "600"); + String resizable = arg("desktop.resizable", "true"); + String fullscreen = arg("desktop.fullscreen", "false"); + String adaptToRetina = arg("desktop.adaptToRetina", "true"); + + String result = template; + result = result.replace("__PACKAGE__", packageName); + result = result.replace("__MAIN_NAME__", mainName); + result = result.replace("__APP_TITLE__", escapeJavaStringLiteral(appTitle)); + result = result.replace("__APP_VERSION__", escapeJavaStringLiteral(appVersion)); + result = result.replace("__APP_WIDTH__", sanitizeInt(width, "800")); + result = result.replace("__APP_HEIGHT__", sanitizeInt(height, "600")); + result = result.replace("__APP_RESIZEABLE__", sanitizeBoolean(resizable, true)); + result = result.replace("__APP_FULLSCREEN__", sanitizeBoolean(fullscreen, false)); + result = result.replace("__APP_ADAPT_TO_RETINA__", sanitizeBoolean(adaptToRetina, true)); + return result; + } + private String arg(String name, String defaultValue) { + String v = properties.getProperty("codename1.arg." + name); + return (v == null || v.isEmpty()) ? defaultValue : v; + } + private String sanitizeInt(String value, String fallback) { + try { + return String.valueOf(Integer.parseInt(value.trim())); + } catch (NumberFormatException ex) { + getLog().warn("Invalid integer in desktop build hint: '" + value + "'. Using " + fallback + "."); + return fallback; + } + } + private String sanitizeBoolean(String value, boolean fallback) { + if (value == null) return String.valueOf(fallback); + String trimmed = value.trim().toLowerCase(); + if ("true".equals(trimmed) || "false".equals(trimmed)) { + return trimmed; + } + getLog().warn("Invalid boolean in desktop build hint: '" + value + "'. Using " + fallback + "."); + return String.valueOf(fallback); + } + private String escapeJavaStringLiteral(String s) { + if (s == null) return ""; + StringBuilder out = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': out.append("\\\\"); break; + case '"': out.append("\\\""); break; + case '\n': out.append("\\n"); break; + case '\r': out.append("\\r"); break; + case '\t': out.append("\\t"); break; + default: + out.append(c); + } + } + return out.toString(); } private void createIconFile(File f, BufferedImage icon, int w, int h) throws IOException { diff --git a/maven/codenameone-maven-plugin/src/main/resources/com/codename1/maven/desktop-app-stub-template.java b/maven/codenameone-maven-plugin/src/main/resources/com/codename1/maven/desktop-app-stub-template.java new file mode 100644 index 0000000000..96dfba5890 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/resources/com/codename1/maven/desktop-app-stub-template.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package __PACKAGE__; + +import com.codename1.impl.javase.JavaSEPort; +import com.codename1.ui.Display; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.io.File; +import java.util.Arrays; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +/** + * Generated desktop wrapper for a Codename One app. Generated at build time by + * the codenameone-maven-plugin's generate-desktop-app-wrapper goal from values + * in codenameone_settings.properties and the codename1.arg.desktop.* build + * hints. Drop a hand-written __MAIN_NAME__Stub.java into javase/src/desktop/java + * to override generation completely. + */ +public class __MAIN_NAME__Stub implements Runnable, WindowListener { + private static final String APP_TITLE = "__APP_TITLE__"; + private static final String APP_NAME = "__MAIN_NAME__"; + private static final String APP_VERSION = "__APP_VERSION__"; + private static final int APP_WIDTH = __APP_WIDTH__; + private static final int APP_HEIGHT = __APP_HEIGHT__; + private static final boolean APP_ADAPT_TO_RETINA = __APP_ADAPT_TO_RETINA__; + private static final boolean APP_RESIZEABLE = __APP_RESIZEABLE__; + private static final boolean APP_FULLSCREEN = __APP_FULLSCREEN__; + public static final String BUILD_KEY = ""; + public static final String PACKAGE_NAME = ""; + public static final String BUILT_BY_USER = ""; + private static final boolean isWindows; + static { + isWindows = File.separatorChar == '\\'; + } + + private static final String[] fontFaces = null; + + private static JFrame frm; + private __MAIN_NAME__ mainApp; + + public static void main(String[] args) { + try { + Class.forName("org.cef.CefApp"); + System.setProperty("cn1.javase.implementation", "cef"); + } catch (Throwable ex){} + + JavaSEPort.setNativeTheme("/NativeTheme.res"); + JavaSEPort.blockMonitors(); + JavaSEPort.setAppHomeDir("." + APP_NAME); + JavaSEPort.setExposeFilesystem(true); + JavaSEPort.setTablet(true); + JavaSEPort.setUseNativeInput(true); + JavaSEPort.setShowEDTViolationStacks(false); + JavaSEPort.setShowEDTWarnings(false); + JavaSEPort.setFullScreen(APP_FULLSCREEN); + + if(fontFaces != null) { + JavaSEPort.setFontFaces(fontFaces[0], fontFaces[1], fontFaces[2]); + } else { + if(isWindows) { + JavaSEPort.setFontFaces("ArialUnicodeMS", "SansSerif", "Monospaced"); + } else { + JavaSEPort.setFontFaces("Arial", "SansSerif", "Monospaced"); + } + } + + frm = new JFrame(APP_TITLE); + Toolkit tk = Toolkit.getDefaultToolkit(); + JavaSEPort.setDefaultPixelMilliRatio(tk.getScreenResolution() / 25.4 * JavaSEPort.getRetinaScale()); + Display.init(frm.getContentPane()); + Display.getInstance().setProperty("build_key", BUILD_KEY); + Display.getInstance().setProperty("package_name", PACKAGE_NAME); + Display.getInstance().setProperty("built_by_user", BUILT_BY_USER); + Display.getInstance().setProperty("AppName", APP_NAME); + Display.getInstance().setProperty("AppVersion", APP_VERSION); + Display.getInstance().setProperty("Platform", System.getProperty("os.name")); + Display.getInstance().setProperty("OSVer", System.getProperty("os.version")); + + SwingUtilities.invokeLater(new __MAIN_NAME__Stub()); + } + + public void run() { + frm.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frm.addWindowListener(this); + ImageIcon ic16 = new ImageIcon(getClass().getResource("/applicationIconImage_16x16.png")); + ImageIcon ic20 = new ImageIcon(getClass().getResource("/applicationIconImage_16x16.png")); + ImageIcon ic32 = new ImageIcon(getClass().getResource("/applicationIconImage_16x16.png")); + ImageIcon ic40 = new ImageIcon(getClass().getResource("/applicationIconImage_16x16.png")); + ImageIcon ic64 = new ImageIcon(getClass().getResource("/applicationIconImage_16x16.png")); + frm.setIconImages(Arrays.asList(ic16.getImage(), ic20.getImage(), ic32.getImage(), ic40.getImage(), ic64.getImage())); + GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + if(APP_FULLSCREEN && gd.isFullScreenSupported()) { + frm.setResizable(false); + frm.setUndecorated(true); + gd.setFullScreenWindow(frm); + } else { + frm.setLocationByPlatform(true); + frm.setResizable(APP_RESIZEABLE); + int w = APP_WIDTH; + int h = APP_HEIGHT; + + frm.getContentPane().setPreferredSize(new java.awt.Dimension(w, h)); + frm.getContentPane().setMinimumSize(new java.awt.Dimension(w, h)); + frm.getContentPane().setMaximumSize(new java.awt.Dimension(w, h)); + + framePrepare(frm); + } + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + if(Display.getInstance().isEdt()) { + mainApp = new __MAIN_NAME__(); + mainApp.init(this); + mainApp.start(); + SwingUtilities.invokeLater(this); + } else { + frameShow(frm); + } + } + }); + } + + private void framePrepare(JFrame frm) { + frm.pack(); + } + + private void frameShow(JFrame frm) { + frm.setVisible(true); + } + + @Override + public void windowOpened(WindowEvent e) { + } + + @Override + public void windowClosing(WindowEvent e) { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + mainApp.stop(); + mainApp.destroy(); + Display.getInstance().exitApplication(); + } + }); + } + + @Override + public void windowClosed(WindowEvent e) { + } + + @Override + public void windowIconified(WindowEvent e) { + } + + @Override + public void windowDeiconified(WindowEvent e) { + } + + @Override + public void windowActivated(WindowEvent e) { + } + + @Override + public void windowDeactivated(WindowEvent e) { + if(APP_FULLSCREEN) { + GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + frm.setExtendedState(JFrame.MAXIMIZED_BOTH); + if(gd.isFullScreenSupported()) { + frm.setResizable(false); + frm.setUndecorated(true); + gd.setFullScreenWindow(frm); + } + } + } +}