From be33f24c9826cf724bd6f31a199f6ffdb0a840fe Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:12:06 -0300 Subject: [PATCH] feat: add LitRenderer.withFunction migration support Close #33 --- pom.xml | 23 +++++ .../LitRendererAsmPostProcessor.java | 86 +++++++++++++++++++ .../LitRendererMigrationExtension.java | 57 ++++++++++++ .../LitRendererMigrationExtensionTest.java | 70 +++++++++++++++ 4 files changed, 236 insertions(+) create mode 100644 src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java create mode 100644 src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java create mode 100644 src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java diff --git a/pom.xml b/pom.xml index 17b9e97..b8001b0 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,12 @@ 9.8 true + + org.ow2.asm + asm-commons + 9.8 + true + tools.jackson.core jackson-databind @@ -142,6 +148,7 @@ 3.1.0 + patch-elemental-nodes process-classes java @@ -158,6 +165,19 @@ + + patch-lit-renderer + process-classes + + java + + + com.flowingcode.vaadin.jsonmigration.LitRendererAsmPostProcessor + + ${project.build.outputDirectory}/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.class + + + @@ -242,6 +262,7 @@ true **/ElementalNodeAsmPostProcessor.java + **/LitRendererAsmPostProcessor.java https://javadoc.io/doc/com.vaadin/vaadin-platform-javadoc/${vaadin.version} @@ -258,6 +279,8 @@ META-INF/VAADIN/config/flow-build-info.json com/flowingcode/vaadin/jsonmigration/ElementalNodeAsmPostProcessor.class com/flowingcode/vaadin/jsonmigration/ElementalNodeAsmPostProcessor$*.class + com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.class + com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor$*.class diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java new file mode 100644 index 0000000..38a30ed --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java @@ -0,0 +1,86 @@ +/*- + * #%L + * Json Migration Helper + * %% + * Copyright (C) 2025 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.jsonmigration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +/** + * Replaces references to the inner LitRenderer stand-in interface with + * com.vaadin.flow.data.renderer.LitRenderer, allowing the class to be compiled against Vaadin 14 + * while referencing the real LitRenderer available in Vaadin 24+. + */ +public class LitRendererAsmPostProcessor { + + private static final String SOURCE = + "com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension$LitRenderer"; + private static final String TARGET = "com/vaadin/flow/data/renderer/LitRenderer"; + + public static void main(String[] args) throws Exception { + for (String arg : args) { + Path classPath = Paths.get(arg); + byte[] original = Files.readAllBytes(classPath); + + ClassReader cr = new ClassReader(original); + ClassWriter cw = new ClassWriter(0); + + Remapper remapper = new Remapper() { + @Override + public String map(String internalName) { + if (internalName.equals(SOURCE)) { + return TARGET; + } + return internalName; + } + }; + + // Drop the InnerClasses entry that ClassRemapper would rewrite to point at the real + // LitRenderer, which is not actually an inner class of LitRendererMigrationExtension. + ClassVisitor filter = new ClassVisitor(Opcodes.ASM9, cw) { + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + if (!TARGET.equals(name)) { + super.visitInnerClass(name, outerName, innerName, access); + } + } + }; + + cr.accept(new ClassRemapper(filter, remapper), 0); + Files.write(classPath, cw.toByteArray()); + System.out.println("Successfully patched: " + classPath.getFileName()); + } + + // Delete the orphaned inner class file produced by the compiler + if (args.length > 0) { + Path innerClass = + Paths.get(args[0]).resolveSibling("LitRendererMigrationExtension$LitRenderer.class"); + if (Files.deleteIfExists(innerClass)) { + System.out.println("Deleted orphaned inner class: " + innerClass.getFileName()); + } + } + } +} diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java new file mode 100644 index 0000000..3506df0 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java @@ -0,0 +1,57 @@ +package com.flowingcode.vaadin.jsonmigration; + +import com.vaadin.flow.function.SerializableBiConsumer; +import com.vaadin.flow.server.Version; +import elemental.json.JsonArray; +import lombok.SneakyThrows; + +/** + * Provides migration support for {@code LitRenderer} event handler registration across + * Vaadin versions. + * + *

In Vaadin 24, {@code LitRenderer.withFunction} receives a {@code JsonArray} from the + * elemental.json API directly. In Vaadin 25+, the argument type changed to a Jackson + * {@code ArrayNode}, so this class wraps the handler to perform the necessary conversion + * transparently. + * + *

The {@code LitRenderer} type referenced here is resolved at runtime via a method handle, + * allowing this library to compile against Vaadin 14 while supporting Vaadin 24+. + * + *

Note: This class is only useful when running on Vaadin 24 or later. On earlier + * versions, {@code LitRenderer} is not available and any attempt to use this class will fail at + * runtime. + */ +public class LitRendererMigrationExtension { + + private class LitRenderer { + LitRenderer withFunction(String functionName, + SerializableBiConsumer handler) { + return null; + } + } + + /** + * Registers an event handler function on the given {@code LitRenderer}. + * + *

On Vaadin 25+, the raw argument passed by the client is a Jackson {@code ArrayNode} rather + * than a {@code JsonArray}, so the handler is wrapped to convert it before forwarding. + * + * @param the bean type of the renderer + * @param renderer the {@code LitRenderer} on which to register the function + * @param name the client-side function name + * @param handler the handler to invoke when the client calls the function; receives the item and + * a {@code JsonArray} of arguments + * @return the same renderer instance, for chaining + */ + @SneakyThrows + public static LitRenderer withFunction(LitRenderer renderer, String name, + SerializableBiConsumer handler) { + SerializableBiConsumer c = handler; + if (Version.getMajorVersion() >= 25) { + c = (source, array) -> handler.accept(source, + (JsonArray) JsonMigration.convertToJsonValue(array)); + } + return renderer.withFunction(name, c); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java b/src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java new file mode 100644 index 0000000..350f7ab --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java @@ -0,0 +1,70 @@ +/*- + * #%L + * Json Migration Helper + * %% + * Copyright (C) 2025 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.jsonmigration; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assume.assumeTrue; + +import com.vaadin.flow.function.SerializableBiConsumer; +import com.vaadin.flow.server.Version; +import elemental.json.JsonArray; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import org.junit.Test; + +public class LitRendererMigrationExtensionTest { + + @Test + public void testWithFunctionRegistersHandler() throws Exception { + assumeTrue("LitRenderer requires Vaadin 24+", Version.getMajorVersion() >= 24); + Class litRendererClass = Class.forName("com.vaadin.flow.data.renderer.LitRenderer"); + Method of = litRendererClass.getMethod("of", String.class); + Object renderer = of.invoke(null, "

"); + + SerializableBiConsumer handler = (source, array) -> {}; + + Method withFunction = LitRendererMigrationExtension.class.getDeclaredMethod( + "withFunction", litRendererClass, String.class, SerializableBiConsumer.class); + withFunction.setAccessible(true); + Object result = withFunction.invoke(null, renderer, "click", handler); + + assertSame(renderer, result); + assertNotNull("Handler for 'click' should be registered on the renderer", + findRegisteredHandler(renderer, "click")); + } + + private static Object findRegisteredHandler(Object renderer, String functionName) + throws Exception { + for (Class c = renderer.getClass(); c != null; c = c.getSuperclass()) { + for (Field field : c.getDeclaredFields()) { + if (Map.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + Map map = (Map) field.get(renderer); + if (map != null && map.containsKey(functionName)) { + return map.get(functionName); + } + } + } + } + return null; + } +}