Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
73eedc2
javafx-compat: list property content events, bound cell graphic, Obse…
shai-almog Oct 30, 2026
9c5f8d8
Mapping processor: canonical names for nested types, map inherited pr…
shai-almog Oct 30, 2026
d278c1a
Default network error path no longer blocks the EDT with a modal prompt
shai-almog Oct 30, 2026
11cd60d
Storage entry size no longer spins forever on a stream that ends
shai-almog Oct 30, 2026
568a1cd
javafx-compat: load http and https images in the background, cached i…
shai-almog Oct 30, 2026
b5d79d7
javafx-compat: wrapText wraps a Label onto several lines
shai-almog Oct 30, 2026
07fbcb5
getStorageEntrySize is abstract; every implementation asks its own st…
shai-almog Oct 30, 2026
5e36cb1
Review fixes: no view leak or poisoned cache in remote images, lazy m…
shai-almog Oct 30, 2026
f5b52f9
WrappingLabel: tabs break lines the same way in layout and paint
shai-almog Oct 30, 2026
bc602dc
javafx-compat: a labeled control with a graphic gives up theme paddin…
shai-almog Oct 30, 2026
9aef058
javafx-compat: a labeled control's minimum width keeps its graphic's …
shai-almog Oct 30, 2026
4f10fa5
javafx-compat: a cell's graphic fits the cell, and rows measure at th…
shai-almog Oct 30, 2026
0cfe9cd
Smooth touch scrolling and press repaints in the JavaFX layer
shai-almog Oct 30, 2026
3a68031
javafx-compat: clip to a shape's outline, fit text fields and lists t…
shai-almog Oct 30, 2026
9520e5e
javafx-compat: keep an opaque region's shadow when a change inside it…
shai-almog Oct 31, 2026
14620ce
javafx-compat: a list's scroll moves its rows without relaying out th…
shai-almog Oct 31, 2026
099f4ff
javafx-compat: keep a phone's scene out of the status bar and fill th…
shai-almog Oct 31, 2026
10b217e
Revert every change outside the JavaFX layer to master
shai-almog Oct 31, 2026
d28ff4b
Revert "javafx-compat: a list's scroll moves its rows without relayin…
shai-almog Oct 31, 2026
64ca97c
javafx-compat: rebuild a shadow picture after the paint, never during it
shai-almog Oct 31, 2026
05955e8
javafx-compat: stretch a touch scroll past its ends and spring back
shai-almog Oct 31, 2026
a477773
javafx-compat: take a drag from its first pixel, and press without re…
shai-almog Oct 31, 2026
0374cea
javafx-compat: survive Codename One's shape clip bug, and re-pad a la…
shai-almog Nov 1, 2026
09f60cf
javafx-compat: keep desktop mouse drags free of overscroll and fling
shai-almog Nov 1, 2026
c1b9b9d
javafx-compat: place a label's graphic without the theme padding a ba…
shai-almog Nov 1, 2026
45b933a
Merge remote-tracking branch 'origin/master' into fx-compat-port-gaps
shai-almog Nov 1, 2026
dcbac86
Reseed the Android DesktopCompatFxData golden for whole-pixel clip edges
shai-almog Nov 1, 2026
e41c9d9
Reseed the macOS DesktopCompatFxData golden for the ListView border edge
shai-almog Nov 1, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,45 @@ public void processClass(AnnotatedClass cls, ProcessorContext ctx) throws Proces
mc.xmlRootName = deriveXmlRoot(mc.simpleName);
}

for (FieldInfo f : cls.getFields()) {
// Properties are the class's own fields plus those it inherits. A
// @Mapped class commonly extends a plain (non-@Mapped, often abstract)
// model base holding id/name/...; reading only the class's own fields
// silently dropped every inherited property in both directions. The
// chain is walked up to java.lang.Object (or the first class not on
// the classpath), base fields first so the JSON order reads naturally,
// and a field hidden by a same-named field lower down is skipped.
// Accessors are looked up from the subclass upward, so an override wins.
List<AnnotatedClass> chain = new ArrayList<AnnotatedClass>();
chain.add(cls);
if (!isRecord) {
String sup = cls.getSuperInternalName();
while (sup != null && !"java/lang/Object".equals(sup) && chain.size() < 64) {
AnnotatedClass sc = ctx.lookupWithDependencies(sup);
if (sc == null) break;
chain.add(sc);
sup = sc.getSuperInternalName();
}
}
List<FieldInfo> allFields = new ArrayList<FieldInfo>();
java.util.Set<String> seenNames = new java.util.HashSet<String>();
for (AnnotatedClass c : chain) {
List<FieldInfo> own = new ArrayList<FieldInfo>();
for (FieldInfo f : c.getFields()) {
if (f.isStatic() || !seenNames.add(f.getName())) continue;
if (c != cls && usesTypeVariable(f)) {
// Base<T>'s `T value` is erased here: its type depends on
// the subclass's type arguments, which this pass does not
// resolve, so mapping it would read or write the wrong type.
ctx.getLog().warn("cn1: @Mapped skipping inherited field " + f.getName()
+ " of " + c.getBinaryName() + " on " + cls.getBinaryName()
+ " (its type is a type variable of the superclass)");
continue;
}
own.add(f);
}
allFields.addAll(0, own);
}
for (FieldInfo f : allFields) {
if (f.isStatic()) continue;
if (f.getName().startsWith("this$")) continue; // inner-class outer ref
boolean useAccessor = false;
Expand All @@ -193,7 +231,7 @@ public void processClass(AnnotatedClass cls, ProcessorContext ctx) throws Proces
// replaced, only its inner value mutated through .set(...) /
// .add(...).
PropertyTypeKind probeKind = PropertyTypeKind.of(f);
String getter = findGetter(cls, f, probeKind);
String getter = findGetter(chain, f, probeKind);
if (getter == null) {
ctx.getLog().debug("cn1: @Mapped skipping private field "
+ f.getName() + " on " + cls.getBinaryName()
Expand All @@ -202,7 +240,7 @@ public void processClass(AnnotatedClass cls, ProcessorContext ctx) throws Proces
}
boolean needsSetter = !(probeKind.kind == PropertyTypeKind.Kind.PROPERTY
|| probeKind.kind == PropertyTypeKind.Kind.LIST_PROPERTY);
String setter = needsSetter ? findSetter(cls, f) : null;
String setter = needsSetter ? findSetter(chain, f) : null;
// A @JsonCreator class sets what it needs through the creator, so a
// getter alone is enough: the field is written, and read only if the
// creator takes it.
Expand Down Expand Up @@ -441,7 +479,51 @@ private static String fieldType(MappedField f) {
}
}

/// Generates the mapper source, then rewrites every nested type it names
/// from its binary form (`pkg.Outer$Inner`) to the canonical form
/// (`pkg.Outer.Inner`). The model keeps binary names because class-file
/// lookups need them, and the emitters append them as-is in dozens of
/// places (class literals, casts, generics, `valueOf`, `instanceof`); but
/// javac does not resolve `Outer$Inner` in source, so a field whose type
/// is a nested class or enum produced a mapper that did not compile.
/// Only the exact names this mapper references are rewritten, and only as
/// whole tokens, so the mapper's own `Outer$InnerCn1Mapper` name (a legal
/// identifier) is left alone.
private static String generateMapperSource(MappedClass mc) {
String src = generateMapperSourceRaw(mc);
java.util.Set<String> nested = new java.util.TreeSet<String>();
collectNestedNames(mc, nested);
for (String bin : nested) {
src = src.replaceAll("(?<![A-Za-z0-9_$.])" + java.util.regex.Pattern.quote(bin)
+ "(?![A-Za-z0-9_$])",
java.util.regex.Matcher.quoteReplacement(bin.replace('$', '.')));
}
return src;
}

private static void collectNestedNames(MappedClass mc, java.util.Set<String> out) {
addNested(mc.binaryName, out);
for (MappedField f : mc.fields) {
if (f.kind != null) {
addNested(f.kind.binaryName, out);
addNested(f.kind.elementBinaryName, out);
}
addNested(f.adapter, out);
addNested(f.adapterCast, out);
addNested(f.elemAdapter, out);
}
for (MappedClass sp : mc.specs) {
collectNestedNames(sp, out);
}
}

private static void addNested(String bin, java.util.Set<String> out) {
if (bin != null && bin.indexOf('$') > 0) {
out.add(bin);
}
}

private static String generateMapperSourceRaw(MappedClass mc) {
StringBuilder sb = new StringBuilder(2048);
if (mc.packageName.length() > 0) {
sb.append("package ").append(mc.packageName).append(";\n\n");
Expand Down Expand Up @@ -1439,6 +1521,24 @@ private static boolean hasPublicNoArgConstructor(AnnotatedClass cls) {
/// `active`), then the literal-prefix variant (`getURL` when the
/// field is `URL`). Returns the method name or null when no accessor
/// matches.
/// [#findGetter] over a class and its superclasses, nearest first.
private static String findGetter(List<AnnotatedClass> chain, FieldInfo f, PropertyTypeKind kind) {
for (AnnotatedClass c : chain) {
String g = findGetter(c, f, kind);
if (g != null) return g;
}
return null;
}

/// [#findSetter] over a class and its superclasses, nearest first.
private static String findSetter(List<AnnotatedClass> chain, FieldInfo f) {
for (AnnotatedClass c : chain) {
String s = findSetter(c, f);
if (s != null) return s;
}
return null;
}

private static String findGetter(AnnotatedClass cls, FieldInfo f, PropertyTypeKind kind) {
String fieldName = f.getName();
String fieldDesc = f.getDescriptor();
Expand Down Expand Up @@ -1490,6 +1590,24 @@ private static String findSetter(AnnotatedClass cls, FieldInfo f) {
return null;
}

/// Whether a field's generic signature names a type variable (`TT;`),
/// anywhere, including as a type argument (`Ljava/util/List<TT;>;`).
static boolean usesTypeVariable(FieldInfo f) {
String sig = f.getSignature();
if (sig == null) {
return false;
}
for (int i = 0; i < sig.length(); i++) {
char c = sig.charAt(i);
if (c == 'T' && (i == 0 || sig.charAt(i - 1) == '<' || sig.charAt(i - 1) == ';'
|| sig.charAt(i - 1) == '[' || sig.charAt(i - 1) == '+' || sig.charAt(i - 1) == '-'
|| sig.charAt(i - 1) == '*')) {
return true;
}
}
return false;
}

private static boolean hasInstanceMethod(AnnotatedClass cls, String name, String descriptor) {
for (MethodInfo m : cls.getMethods()) {
if (!name.equals(m.getName())) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,172 @@ public void pojoRoundTripsThroughGeneratedMapper() throws Exception {
}
}

/// A field typed as a nested class, a list of one, or a nested enum: the
/// generated mapper must name them canonically (`Download.DownloadFile`),
/// since javac does not resolve the binary `Download$DownloadFile` in source.
@Test
public void nestedClassAndEnumFieldsCompileAndRoundTrip() throws Exception {
File classes = tmp.newFolder("classes");
Map<String, String> sources = new LinkedHashMap<String, String>();
sources.put("com.example.Download",
"package com.example;\n"
+ "import com.codename1.annotations.Mapped;\n"
+ "import java.util.List;\n"
+ "@Mapped public class Download {\n"
+ " public DownloadFile main;\n"
+ " public List<DownloadFile> files;\n"
+ " public DownloadType type;\n"
+ " public List<DownloadType> types;\n"
+ " public Download() {}\n"
+ " @Mapped public static class DownloadFile {\n"
+ " public String name;\n"
+ " public DownloadFile() {}\n"
+ " }\n"
+ " public enum DownloadType { FULL, DELTA }\n"
+ "}\n");
JavaSourceCompiler.compile(sources, classes, Arrays.asList(testClassesDir()));
runProcessorOrFail(classes);

try (URLClassLoader cl = childLoader(classes)) {
Class<?> dlCls = cl.loadClass("com.example.Download");
Class<?> fileCls = cl.loadClass("com.example.Download$DownloadFile");
Class<?> typeCls = cl.loadClass("com.example.Download$DownloadType");
Class<?> mapperCls = cl.loadClass("com.example.DownloadCn1Mapper");
Object mapper = mapperCls.newInstance();

Map<String, Object> f1 = new LinkedHashMap<String, Object>();
f1.put("name", "a.bin");
Map<String, Object> f2 = new LinkedHashMap<String, Object>();
f2.put("name", "b.bin");
Map<String, Object> in = new LinkedHashMap<String, Object>();
in.put("main", f1);
in.put("files", Arrays.asList(f1, f2));
in.put("type", "DELTA");
in.put("types", Arrays.asList("FULL"));
// Nested mappers resolve through the registry, so install them.
Class.forName("com.example.Download$DownloadFileCn1Mapper", true, cl)
.getMethod("register").invoke(null);
mapperCls.getMethod("register").invoke(null);
Object d = mapperCls.getMethod("fromMap", Map.class).invoke(mapper, in);
Object main = dlCls.getField("main").get(d);
assertEquals("a.bin", fileCls.getField("name").get(main));
List<?> files = (List<?>) dlCls.getField("files").get(d);
assertEquals(2, files.size());
assertEquals("b.bin", fileCls.getField("name").get(files.get(1)));
Object delta = typeCls.getMethod("valueOf", String.class).invoke(null, "DELTA");
assertEquals(delta, dlCls.getField("type").get(d));

@SuppressWarnings("unchecked")
Map<String, Object> out = (Map<String, Object>) mapperCls
.getMethod("toMap", dlCls).invoke(mapper, d);
assertEquals("DELTA", out.get("type"));
assertEquals(Arrays.asList("FULL"), out.get("types"));
}
}

/// Properties inherited from an abstract, non-@Mapped base class are
/// mapped too, both ways, alongside the subclass's own.
@Test
public void inheritedPropertiesFromPlainBaseRoundTrip() throws Exception {
File classes = tmp.newFolder("classes");
Map<String, String> sources = new LinkedHashMap<String, String>();
sources.put("com.example.ModelObject",
"package com.example;\n"
+ "import java.util.List;\n"
+ "public abstract class ModelObject {\n"
+ " private String id;\n"
+ " private boolean active;\n"
+ " private List<String> personIds;\n"
+ " public String getId() { return id; }\n"
+ " public void setId(String v) { id = v; }\n"
+ " public boolean isActive() { return active; }\n"
+ " public void setActive(boolean v) { active = v; }\n"
+ " public List<String> getPersonIds() { return personIds; }\n"
+ " public void setPersonIds(List<String> v) { personIds = v; }\n"
+ "}\n");
sources.put("com.example.RealWorldApp",
"package com.example;\n"
+ "import com.codename1.annotations.Mapped;\n"
+ "@Mapped public class RealWorldApp extends ModelObject {\n"
+ " private String url;\n"
+ " public RealWorldApp() {}\n"
+ " public String getUrl() { return url; }\n"
+ " public void setUrl(String v) { url = v; }\n"
+ "}\n");
JavaSourceCompiler.compile(sources, classes, Arrays.asList(testClassesDir()));
runProcessorOrFail(classes);

try (URLClassLoader cl = childLoader(classes)) {
Class<?> appCls = cl.loadClass("com.example.RealWorldApp");
Class<?> mapperCls = cl.loadClass("com.example.RealWorldAppCn1Mapper");
Object mapper = mapperCls.newInstance();
Map<String, Object> in = new LinkedHashMap<String, Object>();
in.put("id", "a1");
in.put("active", Boolean.TRUE);
in.put("personIds", Arrays.asList("p1", "p2"));
in.put("url", "https://example.com");
Object app = mapperCls.getMethod("fromMap", Map.class).invoke(mapper, in);
assertEquals("a1", appCls.getMethod("getId").invoke(app));
assertEquals(Boolean.TRUE, appCls.getMethod("isActive").invoke(app));
assertEquals(Arrays.asList("p1", "p2"), appCls.getMethod("getPersonIds").invoke(app));
assertEquals("https://example.com", appCls.getMethod("getUrl").invoke(app));

@SuppressWarnings("unchecked")
Map<String, Object> out = (Map<String, Object>) mapperCls
.getMethod("toMap", appCls).invoke(mapper, app);
assertEquals("a1", out.get("id"));
assertEquals(Boolean.TRUE, out.get("active"));
assertEquals(Arrays.asList("p1", "p2"), out.get("personIds"));
assertEquals("https://example.com", out.get("url"));
}
}

@Test
public void genericSuperclassFieldsAreSkippedNotMistyped() throws Exception {
File classes = tmp.newFolder("classes");
Map<String, String> sources = new LinkedHashMap<String, String>();
sources.put("com.example.Holder",
"package com.example;\n"
+ "import java.util.List;\n"
+ "public abstract class Holder<T> {\n"
+ " private T value;\n"
+ " private List<T> values;\n"
+ " private String label;\n"
+ " public T getValue() { return value; }\n"
+ " public void setValue(T v) { value = v; }\n"
+ " public List<T> getValues() { return values; }\n"
+ " public void setValues(List<T> v) { values = v; }\n"
+ " public String getLabel() { return label; }\n"
+ " public void setLabel(String v) { label = v; }\n"
+ "}\n");
sources.put("com.example.NameHolder",
"package com.example;\n"
+ "import com.codename1.annotations.Mapped;\n"
+ "@Mapped public class NameHolder extends Holder<String> {\n"
+ " public NameHolder() {}\n"
+ "}\n");
JavaSourceCompiler.compile(sources, classes, Arrays.asList(testClassesDir()));
runProcessorOrFail(classes);

try (URLClassLoader cl = childLoader(classes)) {
Class<?> cls = cl.loadClass("com.example.NameHolder");
Class<?> mapperCls = cl.loadClass("com.example.NameHolderCn1Mapper");
Object mapper = mapperCls.newInstance();
Map<String, Object> in = new LinkedHashMap<String, Object>();
in.put("label", "l");
in.put("value", "v");
Object o = mapperCls.getMethod("fromMap", Map.class).invoke(mapper, in);
assertEquals("l", cls.getMethod("getLabel").invoke(o));
// The type-variable fields are left alone rather than mapped as Object.
assertEquals(null, cls.getMethod("getValue").invoke(o));
@SuppressWarnings("unchecked")
Map<String, Object> out = (Map<String, Object>) mapperCls.getMethod("toMap", cls).invoke(mapper, o);
assertEquals("l", out.get("label"));
assertFalse(out.containsKey("value"));
assertFalse(out.containsKey("values"));
}
}

@Test
public void enumFieldsRoundTripThroughJson() throws Exception {
File classes = tmp.newFolder("classes");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,14 @@ public java.lang.String[] getHeaderFields(java.lang.String a0, java.lang.Object
/// Storage held in memory for the life of the test JVM, so code that
/// persists through com.codename1.io.Storage (the AndroidKeyStore) can be
/// read back by a fresh instance.
/// The size of a stored entry, read off the map. The inherited answer
/// counts with skip(), which never ends on these in-memory streams.
@Override
public int getStorageEntrySize(java.lang.String a0) {
byte[] b = STORAGE.get(a0);
return b == null ? -1 : b.length;
}

public static final java.util.Map<String, byte[]> STORAGE =
java.util.Collections.synchronizedMap(new java.util.TreeMap<String, byte[]>());

Expand Down
Loading
Loading