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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.router.Route;
import elemental.json.JsonValue;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
Expand All @@ -38,6 +39,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.SneakyThrows;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
Expand Down Expand Up @@ -163,7 +165,12 @@ public <T extends Component> Class<? extends T> instrumentClass(Class<T> parent)
}

private boolean needsInstrumentation(Class<?> parent) {
return !getInstrumentableMethods(parent).isEmpty();
return needsRouteAnnotation(parent) || !getInstrumentableMethods(parent).isEmpty();
}

private static boolean needsRouteAnnotation(Class<?> parent) {
return parent.isAnnotationPresent(InstrumentedRoute.class)
&& !parent.isAnnotationPresent(Route.class);
}

private boolean hasLegacyVaadin() {
Expand Down Expand Up @@ -289,13 +296,25 @@ private byte[] generateBytecode(String className, Class<?> parent) {
internalParentName,
null);

generateRouteAnnotation(cw, parent);
generateConstructor(cw, internalParentName);
generateClientCallableOverrides(cw, parent, internalClassName, internalParentName);

cw.visitEnd();
return cw.toByteArray();
}

private void generateRouteAnnotation(ClassWriter cw, Class<?> parent) {
if (needsRouteAnnotation(parent)) {
InstrumentedRoute instrumentedRoute = parent.getAnnotation(InstrumentedRoute.class);
AnnotationVisitor av = cw.visitAnnotation(Type.getDescriptor(Route.class), true);
av.visit("value", instrumentedRoute.value());
av.visit("layout", Type.getType(instrumentedRoute.layout()));
av.visit("registerAtStartup", false);
av.visitEnd();
}
}

private void generateConstructor(ClassWriter cw, String internalParentName) {
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Json Migration Helper
* %%
* Copyright (C) 2025 Flowing Code
* 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.
Expand All @@ -20,8 +20,14 @@
package com.flowingcode.vaadin.jsonmigration;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.ParentLayout;
import com.vaadin.flow.router.RouteConfiguration;
import com.vaadin.flow.router.RouterLayout;
import com.vaadin.flow.server.VaadinServiceInitListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Abstract base class for Vaadin service initializers that register instrumented views. Subclasses
Expand All @@ -39,6 +45,10 @@ public abstract class InstrumentationViewInitializer implements VaadinServiceIni
* JsonMigration#instrumentClass(Class)} to get the instrumented class and registers it as a
* Vaadin view with the route derived from the annotation.
*
* The parent layout chain is derived from {@link InstrumentedRoute#layout()} and the {@link
* ParentLayout} annotations of the layout classes, and each layout in the chain is instrumented, or if a layout in the parent chain is not a {@link Component}
* as well.
*
* @param navigationTarget the component class to instrument and register, must be annotated with
* {@link InstrumentedRoute}
* @throws IllegalArgumentException if the navigationTarget is not annotated with {@link
Expand All @@ -55,6 +65,32 @@ protected final void registerInstrumentedRoute(Class<? extends Component> naviga

String route = annotation.value();
navigationTarget = JsonMigration.instrumentClass(navigationTarget);
RouteConfiguration.forApplicationScope().setRoute(route, navigationTarget);

List<Class<? extends RouterLayout>> parentChain = getParentChain(annotation.layout());
RouteConfiguration.forApplicationScope().setRoute(route, navigationTarget, parentChain);
}

static List<Class<? extends RouterLayout>> getParentChain(
Class<? extends RouterLayout> layout) {
List<Class<? extends RouterLayout>> parentChain = Collections.emptyList();
while (layout != UI.class) {
if (parentChain.isEmpty()) {
parentChain = new ArrayList<>();
}
parentChain.add(instrumentLayout(layout));
ParentLayout parentLayout = layout.getAnnotation(ParentLayout.class);
layout = parentLayout != null ? parentLayout.value() : UI.class;
}
return parentChain;
}

private static Class<? extends RouterLayout> instrumentLayout(
Class<? extends RouterLayout> layout) {
if (!Component.class.isAssignableFrom(layout)) {
throw new IllegalArgumentException(
layout.getName() + " must extend " + Component.class.getName());
}
return JsonMigration.instrumentClass(layout.asSubclass(Component.class))
.asSubclass(RouterLayout.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Json Migration Helper
* %%
* Copyright (C) 2025 Flowing Code
* 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.
Expand All @@ -20,6 +20,9 @@
package com.flowingcode.vaadin.jsonmigration;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouterLayout;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -42,4 +45,13 @@
* @return the route path
*/
String value();

/**
* Sets the parent component for the route target component.
* <p>
*
* @return the layout component class used by the route target component.
* @see Route#layout()
*/
Class<? extends RouterLayout> layout() default UI.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ private static JsonMigrationHelper initializeHelper() {
private static final Class<?> BASE_JSON_NODE = lookup_BaseJsonNode();

private static Class<?> lookup_BaseJsonNode() {
if (Version.getMajorVersion() <= 24) {
return null;
}
try {
return Class.forName("tools.jackson.databind.node.BaseJsonNode");
} catch (ClassNotFoundException e) {
Expand Down Expand Up @@ -127,6 +130,12 @@ private static Object invoke(Method method, Object instance, Object... args) {
@SneakyThrows
private static Method lookup_setPropertyJson() {
if (Version.getMajorVersion() > 24) {
if (BASE_JSON_NODE == null) {
throw new IllegalStateException(
"tools.jackson.databind.node.BaseJsonNode is not available on this runtime, "
+ "but is required to resolve Element#setPropertyJson on Vaadin "
+ Version.getFullVersion());
}
return Element.class.getMethod("setPropertyJson", String.class, BASE_JSON_NODE);
} else {
return Element.class.getMethod("setPropertyJson", String.class, JsonValue.class);
Expand Down
1 change: 1 addition & 0 deletions tests-shared/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
<includes>
<include>**/*Test24.java</include>
<include>**/LitRendererMigrationExtensionTest.java</include>
<include>**/InstrumentationViewInitializerTest.java</include>
</includes>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*-
* #%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.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.router.ParentLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouterLayout;
import elemental.json.JsonValue;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;

/**
* Tests for the parent layout chain derived from {@link InstrumentedRoute#layout()} by {@link
* InstrumentationViewInitializer}.
*/
public class InstrumentationViewInitializerTest {

public static class RootLayout extends Div implements RouterLayout {}

@ParentLayout(RootLayout.class)
public static class MiddleLayout extends Div implements RouterLayout {}

@ParentLayout(MiddleLayout.class)
public static class LeafLayout extends Div implements RouterLayout {}

public static class CallableLayout extends Div implements RouterLayout {
@LegacyClientCallable
protected JsonValue test(JsonValue value) {
return value;
}
}

public static class NonComponentLayout implements RouterLayout {
@Override
public Element getElement() {
return null;
}
}

@InstrumentedRoute("without-layout")
public static class RouteWithoutLayout extends Div {}

@InstrumentedRoute(value = "with-layout", layout = LeafLayout.class)
public static class RouteWithLayout extends Div {}

@Route("already-annotated")
@InstrumentedRoute("already-annotated-instrumented")
public static class RouteAlreadyAnnotated extends Div {}

@Test
public void testLayoutDefaultsToUI() {
InstrumentedRoute annotation = RouteWithoutLayout.class.getAnnotation(InstrumentedRoute.class);
assertEquals(UI.class, annotation.layout());
}

@Test
public void testDefaultLayoutYieldsEmptyParentChain() {
InstrumentedRoute annotation = RouteWithoutLayout.class.getAnnotation(InstrumentedRoute.class);
assertTrue(InstrumentationViewInitializer.getParentChain(annotation.layout()).isEmpty());
}

@Test
public void testSingleLayout() {
List<Class<? extends RouterLayout>> chain =
InstrumentationViewInitializer.getParentChain(RootLayout.class);
assertEquals(Arrays.asList(JsonMigration.instrumentClass(RootLayout.class)), chain);
}

@Test
public void testParentLayoutChainIsWalked() {
InstrumentedRoute annotation = RouteWithLayout.class.getAnnotation(InstrumentedRoute.class);
List<Class<? extends RouterLayout>> chain =
InstrumentationViewInitializer.getParentChain(annotation.layout());
assertEquals(
Arrays.asList(
JsonMigration.instrumentClass(LeafLayout.class),
JsonMigration.instrumentClass(MiddleLayout.class),
JsonMigration.instrumentClass(RootLayout.class)),
chain);
}

@Test
public void testLayoutIsInstrumented() {
List<Class<? extends RouterLayout>> chain =
InstrumentationViewInitializer.getParentChain(CallableLayout.class);
assertEquals(1, chain.size());
Class<? extends RouterLayout> instrumented = chain.get(0);
assertEquals(JsonMigration.instrumentClass(CallableLayout.class), instrumented);
assertTrue(CallableLayout.class.isAssignableFrom(instrumented));
assertTrue(RouterLayout.class.isAssignableFrom(instrumented));
}

@Test(expected = IllegalArgumentException.class)
public void testNonComponentLayoutThrows() {
InstrumentationViewInitializer.getParentChain(NonComponentLayout.class);
}

@Test
public void testRouteAnnotationIsAdded() {
Class<?> instrumented = JsonMigration.instrumentClass(RouteWithoutLayout.class);
assertNotEquals(RouteWithoutLayout.class, instrumented);
Route route = instrumented.getAnnotation(Route.class);
assertNotNull(route);
assertEquals("without-layout", route.value());
assertEquals(UI.class, route.layout());
assertFalse(route.registerAtStartup());
}

@Test
public void testRouteAnnotationPropagatesLayout() {
Class<?> instrumented = JsonMigration.instrumentClass(RouteWithLayout.class);
Route route = instrumented.getAnnotation(Route.class);
assertNotNull(route);
assertEquals("with-layout", route.value());
assertEquals(LeafLayout.class, route.layout());
assertFalse(route.registerAtStartup());
}

@Test
public void testExistingRouteAnnotationIsNotReplaced() {
assertEquals(
RouteAlreadyAnnotated.class, JsonMigration.instrumentClass(RouteAlreadyAnnotated.class));
}
}
1 change: 1 addition & 0 deletions tests-v25/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<include>**/*Test25.java</include>
<include>**/JsonMigrationHelper25Test.java</include>
<include>**/LitRendererMigrationExtensionTest.java</include>
<include>**/InstrumentationViewInitializerTest.java</include>
</includes>
</configuration>
</plugin>
Expand Down
Loading