Skip to content
Open
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
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
<version>9.8</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.8</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down Expand Up @@ -142,6 +148,7 @@
<version>3.1.0</version>
<executions>
<execution>
<id>patch-elemental-nodes</id>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
Expand All @@ -158,6 +165,19 @@
</arguments>
</configuration>
</execution>
<execution>
<id>patch-lit-renderer</id>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>com.flowingcode.vaadin.jsonmigration.LitRendererAsmPostProcessor</mainClass>
<arguments>
<argument>${project.build.outputDirectory}/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.class</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
Expand Down Expand Up @@ -242,6 +262,7 @@
<failOnWarnings>true</failOnWarnings>
<sourceFileExcludes>
<sourceFileExclude>**/ElementalNodeAsmPostProcessor.java</sourceFileExclude>
<sourceFileExclude>**/LitRendererAsmPostProcessor.java</sourceFileExclude>
</sourceFileExcludes>
<links>
<link>https://javadoc.io/doc/com.vaadin/vaadin-platform-javadoc/${vaadin.version}</link>
Expand All @@ -258,6 +279,8 @@
<exclude>META-INF/VAADIN/config/flow-build-info.json</exclude>
<exclude>com/flowingcode/vaadin/jsonmigration/ElementalNodeAsmPostProcessor.class</exclude>
<exclude>com/flowingcode/vaadin/jsonmigration/ElementalNodeAsmPostProcessor$*.class</exclude>
<exclude>com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.class</exclude>
<exclude>com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor$*.class</exclude>
</excludes>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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+.
*
* <p><b>Note:</b> 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<SOURCE> {
LitRenderer<SOURCE> withFunction(String functionName,
SerializableBiConsumer<SOURCE, ?> handler) {
return null;
}
}

/**
* Registers an event handler function on the given {@code LitRenderer}.
*
* <p>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 <SOURCE> 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 <SOURCE> LitRenderer<SOURCE> withFunction(LitRenderer<SOURCE> renderer, String name,
SerializableBiConsumer<SOURCE, JsonArray> handler) {
SerializableBiConsumer<SOURCE, ?> c = handler;
if (Version.getMajorVersion() >= 25) {
c = (source, array) -> handler.accept(source,
(JsonArray) JsonMigration.convertToJsonValue(array));
}
return renderer.withFunction(name, c);
}

}
Original file line number Diff line number Diff line change
@@ -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, "<div></div>");

SerializableBiConsumer<String, JsonArray> 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;
}
}
Loading