Skip to content

Commit 68ce82e

Browse files
javier-godoypaodb
authored andcommitted
feat: add support for private callable methods
1 parent 17c0dfd commit 68ce82e

28 files changed

+812
-11
lines changed

src/main/java/com/flowingcode/vaadin/jsonmigration/ClassInstrumentationUtil.java

Lines changed: 178 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import java.lang.reflect.Constructor;
2929
import java.lang.reflect.Method;
3030
import java.lang.reflect.Modifier;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.HashMap;
3134
import java.util.List;
3235
import java.util.Map;
3336
import java.util.WeakHashMap;
@@ -149,10 +152,10 @@ private boolean hasLegacyVaadin() {
149152
return version <= 24;
150153
}
151154

152-
private static Stream<Method> getDeclaredCallables(Class<?> parent) {
153-
return Stream.of(parent.getDeclaredMethods()).filter(method -> {
155+
private static Stream<Method> getDeclaredCallables(Class<?> clazz) {
156+
return Stream.of(clazz.getDeclaredMethods()).filter(method -> {
154157
int modifiers = method.getModifiers();
155-
if (!Modifier.isStatic(modifiers) && !Modifier.isPrivate(modifiers)) {
158+
if (!Modifier.isStatic(modifiers)) {
156159
boolean isCallable = method.isAnnotationPresent(ClientCallable.class);
157160
boolean isLegacyCallable = method.isAnnotationPresent(LegacyClientCallable.class);
158161
return isCallable || isLegacyCallable;
@@ -161,8 +164,27 @@ private static Stream<Method> getDeclaredCallables(Class<?> parent) {
161164
});
162165
}
163166

167+
private static Stream<Method> getAllCallables(Class<?> baseClass) {
168+
Map<String, Method> map = new HashMap<>();
169+
for (Class<?> clazz = baseClass; clazz != Component.class; clazz = clazz.getSuperclass()) {
170+
getDeclaredCallables(clazz).forEach(method -> {
171+
Method existing = map.get(method.getName());
172+
if (existing == null) {
173+
map.put(method.getName(), method);
174+
} else if (!Arrays.equals(existing.getParameterTypes(), method.getParameterTypes())) {
175+
String msg = String.format("There may be only one handler method with the given name. "
176+
+ "Class '%s' (considering its superclasses) "
177+
+ "contains several handler methods with the same name: '%s'",
178+
baseClass.getName(), method.getName());
179+
throw new IllegalStateException(msg);
180+
}
181+
});
182+
}
183+
return map.values().stream();
184+
}
185+
164186
private List<Method> getInstrumentableMethods(Class<?> parent) {
165-
return getDeclaredCallables(parent).filter(method -> {
187+
return getAllCallables(parent).filter(method -> {
166188
boolean isCallable = method.isAnnotationPresent(ClientCallable.class);
167189
boolean isLegacyCallable = method.isAnnotationPresent(LegacyClientCallable.class);
168190
boolean hasJsonValueReturn = JsonValue.class.isAssignableFrom(method.getReturnType());
@@ -238,7 +260,7 @@ private byte[] generateBytecode(String className, Class<?> parent) {
238260
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalClassName, null, internalParentName, null);
239261

240262
generateConstructor(cw, internalParentName);
241-
generateClientCallableOverrides(cw, parent, internalParentName);
263+
generateClientCallableOverrides(cw, parent, internalClassName, internalParentName);
242264

243265
cw.visitEnd();
244266
return cw.toByteArray();
@@ -255,13 +277,99 @@ private void generateConstructor(ClassWriter cw, String internalParentName) {
255277
}
256278

257279
private void generateClientCallableOverrides(ClassWriter cw, Class<?> parent,
258-
String internalParentName) {
280+
String internalClassName, String internalParentName) {
281+
List<String> privateMethodNames = new ArrayList<>();
259282
for (Method method : getInstrumentableMethods(parent)) {
260-
generateMethodOverride(cw, method, internalParentName);
283+
if (Modifier.isPrivate(method.getModifiers())) {
284+
privateMethodNames.add(method.getName());
285+
createLookupHelper(cw, method);
286+
cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL,
287+
method.getName(),
288+
Type.getDescriptor(MethodHandle.class), null, null);
289+
}
290+
generateMethodOverride(cw, method, internalClassName, internalParentName);
261291
}
292+
293+
if (!privateMethodNames.isEmpty()) {
294+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
295+
mv.visitCode();
296+
for (String name : privateMethodNames) {
297+
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
298+
internalClassName,
299+
"lookup_"+name,
300+
"()"+Type.getDescriptor(MethodHandle.class),
301+
false);
302+
mv.visitFieldInsn(Opcodes.PUTSTATIC,
303+
internalClassName, name,
304+
"Ljava/lang/invoke/MethodHandle;");
305+
}
306+
mv.visitInsn(Opcodes.RETURN);
307+
mv.visitMaxs(0, 0);
308+
mv.visitEnd();
309+
}
310+
}
311+
312+
private void createLookupHelper(ClassWriter cw, Method method) {
313+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
314+
"lookup_" + method.getName(), "()" + Type.getDescriptor(MethodHandle.class), null, null);
315+
316+
// Invoke static MethodHandles.lookup()
317+
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
318+
"java/lang/invoke/MethodHandles",
319+
"lookup",
320+
"()Ljava/lang/invoke/MethodHandles$Lookup;",
321+
false);
322+
323+
// Load the Owner class
324+
mv.visitLdcInsn(Type.getType(method.getDeclaringClass()));
325+
326+
// Load the Method Name
327+
mv.visitLdcInsn(method.getName());
328+
329+
// Create Class[] array
330+
Class<?> argTypes[] = method.getParameterTypes();
331+
pushInt(mv, (short) argTypes.length);
332+
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class");
333+
334+
// Load the specific Class objects and store in array
335+
for (short i = 0; i < argTypes.length; i++) {
336+
mv.visitInsn(Opcodes.DUP);
337+
pushInt(mv, i);
338+
loadClassConstant(mv, argTypes[i]);
339+
mv.visitInsn(Opcodes.AASTORE);
340+
}
341+
342+
// Invoke getDeclaredMethod
343+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
344+
"java/lang/Class",
345+
"getDeclaredMethod",
346+
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;",
347+
false);
348+
349+
// Invoke method.setAccessible(true)
350+
mv.visitInsn(Opcodes.DUP);
351+
mv.visitInsn(Opcodes.ICONST_1);
352+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
353+
"java/lang/reflect/Method",
354+
"setAccessible",
355+
"(Z)V",
356+
false);
357+
358+
// Invoke Lookup.unresolve(method)
359+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
360+
"java/lang/invoke/MethodHandles$Lookup",
361+
"unreflect",
362+
"(Ljava/lang/reflect/Method;)Ljava/lang/invoke/MethodHandle;",
363+
false);
364+
365+
// Return result
366+
mv.visitInsn(Opcodes.ARETURN);
367+
368+
mv.visitMaxs(0, 0);
369+
mv.visitEnd();
262370
}
263371

264-
private void generateMethodOverride(ClassWriter cw, Method method, String internalParentName) {
372+
private void generateMethodOverride(ClassWriter cw, Method method, String internalClassName, String internalParentName) {
265373
boolean hasJsonValueReturn = !hasLegacyVaadin() && JsonValue.class.isAssignableFrom(method.getReturnType());
266374
boolean hasJsonValueParams = !hasLegacyVaadin() && hasJsonValueParameters(method);
267375

@@ -275,6 +383,14 @@ private void generateMethodOverride(ClassWriter cw, Method method, String intern
275383
mv.visitAnnotation(Type.getDescriptor(ClientCallable.class), true);
276384
mv.visitCode();
277385

386+
boolean isPrivate = Modifier.isPrivate(method.getModifiers());
387+
if (isPrivate) {
388+
// Load MethodHandle from static field
389+
mv.visitFieldInsn(Opcodes.GETSTATIC,
390+
internalClassName, method.getName(),
391+
"Ljava/lang/invoke/MethodHandle;");
392+
}
393+
278394
// Load 'this'
279395
mv.visitVarInsn(Opcodes.ALOAD, 0);
280396

@@ -301,9 +417,24 @@ private void generateMethodOverride(ClassWriter cw, Method method, String intern
301417
}
302418
}
303419

304-
// Call super.methodName(params) with original descriptor
305-
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, internalParentName, method.getName(), superDescriptor,
306-
false);
420+
if (isPrivate) {
421+
// Call private method
422+
String descriptor =
423+
"(" + Type.getDescriptor(method.getDeclaringClass())
424+
+ superDescriptor.substring(1);
425+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
426+
"java/lang/invoke/MethodHandle",
427+
"invokeExact",
428+
descriptor,
429+
false);
430+
} else {
431+
// Call super.methodName(params) with original descriptor
432+
mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
433+
internalParentName,
434+
method.getName(),
435+
superDescriptor,
436+
false);
437+
}
307438

308439
if (hasJsonValueReturn) {
309440
// Store result in local variable
@@ -339,6 +470,42 @@ private void generateMethodOverride(ClassWriter cw, Method method, String intern
339470
mv.visitEnd();
340471
}
341472

473+
private void pushInt(MethodVisitor mv, short value) {
474+
if (value >= -1 && value <= 5) {
475+
mv.visitInsn(Opcodes.ICONST_0 + value);
476+
} else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
477+
mv.visitIntInsn(Opcodes.BIPUSH, value);
478+
} else {
479+
mv.visitIntInsn(Opcodes.SIPUSH, value);
480+
}
481+
}
482+
483+
private void loadClassConstant(MethodVisitor mv, Class<?> clazz) {
484+
if (clazz.isPrimitive()) {
485+
if (clazz == int.class) {
486+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;");
487+
} else if (clazz == boolean.class) {
488+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;");
489+
} else if (clazz == byte.class) {
490+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;");
491+
} else if (clazz == char.class) {
492+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;");
493+
} else if (clazz == short.class) {
494+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;");
495+
} else if (clazz == float.class) {
496+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;");
497+
} else if (clazz == long.class) {
498+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;");
499+
} else if (clazz == double.class) {
500+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;");
501+
} else {
502+
throw new IllegalArgumentException("Unsupported type: " + clazz);
503+
}
504+
} else {
505+
mv.visitLdcInsn(Type.getType(clazz));
506+
}
507+
}
508+
342509
private int loadParameter(MethodVisitor mv, Class<?> paramType, int localVarIndex) {
343510
if (!paramType.isPrimitive()) {
344511
mv.visitVarInsn(Opcodes.ALOAD, localVarIndex);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
public class ExtendsLegacyClientCallablePrivate__V extends LegacyClientCallablePrivate__V {
4+
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
public class LegacyClientCallablePrivate_D__V extends BaseClientCallable {
4+
5+
@LegacyClientCallable
6+
private void test(double arg) {
7+
trace();
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
public class LegacyClientCallablePrivate_I__V extends BaseClientCallable {
4+
5+
@LegacyClientCallable
6+
private void test(int arg) {
7+
trace();
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
import elemental.json.JsonArray;
4+
5+
public class LegacyClientCallablePrivate_JsonArray__V extends BaseClientCallable {
6+
7+
@LegacyClientCallable
8+
private void test(JsonArray arg) {
9+
trace();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
import elemental.json.JsonBoolean;
4+
5+
public class LegacyClientCallablePrivate_JsonBoolean__V extends BaseClientCallable {
6+
7+
@LegacyClientCallable
8+
private void test(JsonBoolean arg) {
9+
trace();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
import elemental.json.JsonNull;
4+
5+
public class LegacyClientCallablePrivate_JsonNull__V extends BaseClientCallable {
6+
7+
@LegacyClientCallable
8+
private void test(JsonNull arg) {
9+
trace();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
import elemental.json.JsonNumber;
4+
5+
public class LegacyClientCallablePrivate_JsonNumber__V extends BaseClientCallable {
6+
7+
@LegacyClientCallable
8+
private void test(JsonNumber arg) {
9+
trace();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
import elemental.json.JsonObject;
4+
5+
public class LegacyClientCallablePrivate_JsonObject__V extends BaseClientCallable {
6+
7+
@LegacyClientCallable
8+
private void test(JsonObject arg) {
9+
trace();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flowingcode.vaadin.jsonmigration;
2+
3+
import elemental.json.JsonString;
4+
5+
public class LegacyClientCallablePrivate_JsonString__V extends BaseClientCallable {
6+
7+
@LegacyClientCallable
8+
private void test(JsonString arg) {
9+
trace();
10+
}
11+
}

0 commit comments

Comments
 (0)