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
20 changes: 20 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,26 @@ jobs:
-Dcodename1.platform=javase \
-Dcn1.binaries="${CN1_BINARIES}" \
-pl codenameone-maven-plugin -am -Plocal-dev-javase test
- name: Run JavaSE port unit tests
# Surface regressions in the simulator-side helpers (CSSWatcher
# localization, MavenUtils designer-jar resolution, JavaSEPort font
# mapping). Without this step the tests in maven/javase compile but
# never execute on PRs. We install the javase module's transitive
# deps first (sqlite-jdbc, factory, etc. -- earlier steps don't
# publish them to the local repo) so the test invocation can resolve
# them, then run javase's tests in isolation.
working-directory: maven
env:
CN1_BINARIES: ${{ github.workspace }}/maven/target/cn1-binaries
run: |
mvn -B -Dmaven.javadoc.skip=true \
-Dcodename1.platform=javase \
-Dcn1.binaries="${CN1_BINARIES}" \
-pl javase -am -Plocal-dev-javase -DskipTests install
mvn -B -Dmaven.javadoc.skip=true \
-Dcodename1.platform=javase \
-Dcn1.binaries="${CN1_BINARIES}" \
-pl javase -Plocal-dev-javase test
- name: Run SpotBugs for ports and Maven plugin
if: ${{ matrix.java-version == 8 }}
working-directory: maven
Expand Down
39 changes: 39 additions & 0 deletions CodenameOne/src/com/codename1/ui/plaf/UIManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,7 @@ private void resetThemeProps(Hashtable installedTheme) {
/// - `themeProps`: the properties of the given theme
public void addThemeProps(Hashtable themeProps) {
if (accessible) {
dropSupersededBindings(themeProps);
buildTheme(themeProps);
styles.clear();
selectedStyles.clear();
Expand All @@ -1508,6 +1509,44 @@ public void addThemeProps(Hashtable themeProps) {
}
}

/// CSSWatcher's live-reload funnels every recompile through
/// [#addThemeProps], which never clears [#themeConstants]. When a user
/// replaces a `var()`-bound CSS rule with a literal, the recompiled
/// theme.res carries the new style value but no longer emits the
/// matching `@cn1-bind:<key>` entry. Without intervention the stale
/// binding left in `themeConstants` would let [#applyThemeBindings]
/// stomp the user's literal change back to the previous binding's
/// resolved value -- visibly hiding every CSS edit.
///
/// This pre-pass runs only on the overlay entry point (`addThemeProps`),
/// not on the full reset path ([#setThemePropsImpl] -> [#buildTheme],
/// which clears `themeConstants` itself, and the `@includeNativeBool`
/// layered initial load whose existing screenshots depend on bindings
/// staying in place). For each style key being re-set by the incoming
/// load that does NOT re-assert its binding, drop the matching binding
/// from `themeConstants` so the new literal wins.
private void dropSupersededBindings(Hashtable themeProps) {
if (themeProps == null || themeConstants == null || themeConstants.isEmpty()) {
return;
}
Enumeration e = themeProps.keys();
while (e.hasMoreElements()) {
Object keyObj = e.nextElement();
if (!(keyObj instanceof String)) {
continue;
}
String key = (String) keyObj;
if (key.startsWith("@")) {
continue;
}
String boundConstant = "cn1-bind:" + key;
if (themeConstants.containsKey(boundConstant)
&& !themeProps.containsKey("@" + boundConstant)) {
themeConstants.remove(boundConstant);
}
}
}

/// Scales the font sizes of the current theme by the given factor, e.g.
/// a factor of 1.2 increases all font sizes by 20% and a factor of 0.8
/// decreases them by 20%. Only fonts that support scaling (TTF or native:
Expand Down
98 changes: 94 additions & 4 deletions Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
import com.codename1.io.Log;
import com.codename1.ui.Display;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
*
Expand Down Expand Up @@ -85,7 +92,20 @@ public static File findDesignerJarInM2() {
if (location == null) {
return null;
}
File coreJar = new File(location.toURI());
return findDesignerJarInM2(new File(location.toURI()));
} catch (Throwable t) {
// Best-effort lookup. Any unexpected layout means we can't resolve via m2.
}
return null;
}

/**
* Test seam for {@link #findDesignerJarInM2()}: takes the codenameone-core jar
* path explicitly so the resolution logic can be exercised against a fake m2
* layout in a temp directory.
*/
static File findDesignerJarInM2(File coreJar) {
try {
// Expected layout: <repo>/com/codenameone/codenameone-core/<version>/codenameone-core-<version>.jar
File versionDir = coreJar.getParentFile();
if (versionDir == null) return null;
Expand All @@ -98,16 +118,86 @@ public static File findDesignerJarInM2() {
}
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;
// The published jar-with-dependencies artifact is *not* directly runnable:
// maven/designer/pom.xml's antrun step renames the shaded jar to
// designer_1.jar and re-zips it, so this file is a zip wrapper containing
// a single designer_1.jar entry with no top-level Main-Class manifest.
// AbstractCN1Mojo.getDesignerJar (in the maven plugin) unzips it on demand
// and returns the inner jar; we mirror that here so the CSSWatcher
// fallback path receives a path that `java -jar` can actually launch.
File wrapperZip = new File(designerVersionDir, "codenameone-designer-" + version + "-jar-with-dependencies.jar");
if (!wrapperZip.isFile()) {
return null;
}
File extracted = new File(wrapperZip.getParentFile(), wrapperZip.getName() + "-extracted");
File innerJar = new File(extracted, "designer_1.jar");
if (!innerJar.isFile() || innerJar.lastModified() < wrapperZip.lastModified()) {
extractInnerJar(wrapperZip, extracted);
}
if (innerJar.isFile()) {
return innerJar;
}
} catch (Throwable t) {
// Best-effort lookup. Any unexpected layout means we can't resolve via m2.
}
return null;
}

private static final String INNER_JAR_NAME = "designer_1.jar";

/**
* Extracts the single expected inner jar from the designer wrapper artifact.
*
* <p>The wrapper produced by {@code maven/designer/pom.xml} contains exactly
* one entry named {@code designer_1.jar} at the root. To stay safe against
* Zip Slip even if an unexpected artifact is dropped in m2, this method:
* (1) writes only to a single, fixed destination path under {@code destDir}
* (never derived from the archive's entry name), and (2) skips any entry
* whose name isn't the literal expected filename. A malicious entry like
* {@code ../../etc/passwd} therefore never participates in path
* construction; in the worst case the loop finds no match and throws.</p>
*/
private static void extractInnerJar(File wrapperZip, File destDir) throws IOException {
if (!destDir.exists() && !destDir.mkdirs() && !destDir.isDirectory()) {
throw new IOException("Could not create designer extraction directory: " + destDir.getAbsolutePath());
}
File innerJar = new File(destDir, INNER_JAR_NAME);
InputStream in = new FileInputStream(wrapperZip);
try {
ZipInputStream zis = new ZipInputStream(in);
try {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
if (!INNER_JAR_NAME.equals(entry.getName())) {
// Unexpected entry. Skip it rather than materialize a
// file path derived from untrusted archive metadata.
continue;
}
OutputStream fos = new FileOutputStream(innerJar);
try {
byte[] buf = new byte[8192];
int n;
while ((n = zis.read(buf)) > 0) {
fos.write(buf, 0, n);
}
} finally {
fos.close();
}
return;
}
throw new IOException("Wrapper zip does not contain a " + INNER_JAR_NAME
+ " entry: " + wrapperZip.getAbsolutePath());
} finally {
zis.close();
}
} finally {
in.close();
}
}

public static boolean isRunningInJDK() {
if (!isRunningInJDKChecked) {
isRunningInJDKChecked = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,77 @@ public void invalidColorOverrideLeavesDefaultIntact() {
b.setUIID("Button");
assertEquals(0x007aff, b.getUnselectedStyle().getFgColor());
}

/// Simulates CSSWatcher's live-reload sequence: an initial theme load
/// (`setThemeProps`) followed by `addThemeProps` carrying a fresh
/// theme.res whose source CSS has dropped a `var()` reference in favor of
/// a literal. The reloaded Hashtable contains the new literal value for
/// `Button.fgColor` but no `@cn1-bind:Button.fgColor` entry, because the
/// CSS compiler only emits bindings for properties that still reference a
/// `var()`. The stale binding left in `themeConstants` from the first
/// load must not stomp the user's new literal value.
@Test
public void cssReloadDropsStaleBindingWhenRuleBecomesLiteral() {
Hashtable initial = new Hashtable();
initial.put("Button.fgColor", "ff0000");
initial.put("@cn1-bind:Button.fgColor", "accent-color");
initial.put("@accent-color", "ff0000");
UIManager.getInstance().setThemeProps(initial);

Hashtable reloaded = new Hashtable();
reloaded.put("Button.fgColor", "0000ff");
UIManager.getInstance().addThemeProps(reloaded);

Button b = new Button();
b.setUIID("Button");
assertEquals(0x0000ff, b.getUnselectedStyle().getFgColor());
}

/// Companion to [#cssReloadDropsStaleBindingWhenRuleBecomesLiteral]: when
/// the reload Hashtable carries BOTH the property and a fresh binding,
/// the binding still applies. This guards against an over-eager fix that
/// would drop bindings every time a style key shows up in the reload.
@Test
public void cssReloadKeepsBindingWhenStillEmittedTogether() {
Hashtable initial = new Hashtable();
initial.put("Button.fgColor", "ff0000");
initial.put("@cn1-bind:Button.fgColor", "accent-color");
initial.put("@accent-color", "ff0000");
UIManager.getInstance().setThemeProps(initial);

Hashtable reloaded = new Hashtable();
reloaded.put("Button.fgColor", "0000ff");
reloaded.put("@cn1-bind:Button.fgColor", "accent-color");
reloaded.put("@accent-color", "00ff00");
UIManager.getInstance().addThemeProps(reloaded);

Button b = new Button();
b.setUIID("Button");
assertEquals(0x00ff00, b.getUnselectedStyle().getFgColor());
}

/// A pure override Hashtable (no style keys, only a single `@varname`
/// constant) must not invalidate the existing bindings. This is the
/// canonical "user rebrands the accent" call path and the existing
/// [#boundThemeKeyPicksUpAccentOverride] covers a single hop; this test
/// adds a follow-up override to make sure repeated retunes keep working.
@Test
public void overrideOnlyReloadKeepsBindings() {
Hashtable initial = new Hashtable();
initial.put("Button.fgColor", "007aff");
initial.put("@cn1-bind:Button.fgColor", "accent-color");
UIManager.getInstance().setThemeProps(initial);

Hashtable firstOverride = new Hashtable();
firstOverride.put("@accent-color", "ff2d95");
UIManager.getInstance().addThemeProps(firstOverride);

Hashtable secondOverride = new Hashtable();
secondOverride.put("@accent-color", "00aa66");
UIManager.getInstance().addThemeProps(secondOverride);

Button b = new Button();
b.setUIID("Button");
assertEquals(0x00aa66, b.getUnselectedStyle().getFgColor());
}
}
11 changes: 11 additions & 0 deletions maven/javase/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@
</resource>
</resources>
<plugins>
<plugin>
<!-- The parent pom pins surefire to 2.21.0 which lacks the
JUnit Jupiter provider this module's tests rely on (the
pre-existing CSSWatcherTest and the MavenUtilsTest added
alongside the CSSWatcher live-reload fixes). Bump to a
surefire version with native Jupiter discovery so these
tests actually execute. -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
Expand Down
Loading
Loading