Skip to content

Commit 0937288

Browse files
committed
feat: add support for private callable methods
1 parent 17c0dfd commit 0937288

28 files changed

+806
-11
lines changed

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

Lines changed: 179 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,28 @@ 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+
&& !Arrays.equals(existing.getParameterTypes(), method.getParameterTypes())) {
174+
String msg = String.format("There may be only one handler method with the given name. "
175+
+ "Class '%s' (considering its superclasses) "
176+
+ "contains several handler methods with the same name: '%s'",
177+
baseClass.getName(), method.getName());
178+
throw new IllegalStateException(msg);
179+
} else {
180+
map.put(method.getName(), method);
181+
}
182+
});
183+
}
184+
return map.values().stream();
185+
}
186+
164187
private List<Method> getInstrumentableMethods(Class<?> parent) {
165-
return getDeclaredCallables(parent).filter(method -> {
188+
return getAllCallables(parent).filter(method -> {
166189
boolean isCallable = method.isAnnotationPresent(ClientCallable.class);
167190
boolean isLegacyCallable = method.isAnnotationPresent(LegacyClientCallable.class);
168191
boolean hasJsonValueReturn = JsonValue.class.isAssignableFrom(method.getReturnType());
@@ -238,7 +261,7 @@ private byte[] generateBytecode(String className, Class<?> parent) {
238261
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalClassName, null, internalParentName, null);
239262

240263
generateConstructor(cw, internalParentName);
241-
generateClientCallableOverrides(cw, parent, internalParentName);
264+
generateClientCallableOverrides(cw, parent, internalClassName, internalParentName);
242265

243266
cw.visitEnd();
244267
return cw.toByteArray();
@@ -255,13 +278,99 @@ private void generateConstructor(ClassWriter cw, String internalParentName) {
255278
}
256279

257280
private void generateClientCallableOverrides(ClassWriter cw, Class<?> parent,
258-
String internalParentName) {
281+
String internalClassName, String internalParentName) {
282+
List<String> privateMethodNames = new ArrayList<>();
259283
for (Method method : getInstrumentableMethods(parent)) {
260-
generateMethodOverride(cw, method, internalParentName);
284+
if (Modifier.isPrivate(method.getModifiers())) {
285+
privateMethodNames.add(method.getName());
286+
createLookupHelper(cw, method);
287+
cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL,
288+
method.getName(),
289+
Type.getDescriptor(MethodHandle.class), null, null);
290+
}
291+
generateMethodOverride(cw, method, internalClassName, internalParentName);
292+
}
293+
294+
if (!privateMethodNames.isEmpty()) {
295+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
296+
mv.visitCode();
297+
for (String name : privateMethodNames) {
298+
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
299+
internalClassName,
300+
"lookup_"+name,
301+
"()"+Type.getDescriptor(MethodHandle.class),
302+
false);
303+
mv.visitFieldInsn(Opcodes.PUTSTATIC,
304+
internalClassName, name,
305+
"Ljava/lang/invoke/MethodHandle;");
306+
}
307+
mv.visitInsn(Opcodes.RETURN);
308+
mv.visitMaxs(0, 0);
309+
mv.visitEnd();
261310
}
262311
}
263312

264-
private void generateMethodOverride(ClassWriter cw, Method method, String internalParentName) {
313+
private void createLookupHelper(ClassWriter cw, Method method) {
314+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
315+
"lookup_" + method.getName(), "()" + Type.getDescriptor(MethodHandle.class), null, null);
316+
317+
// Invoke static MethodHandles.lookup()
318+
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
319+
"java/lang/invoke/MethodHandles",
320+
"lookup",
321+
"()Ljava/lang/invoke/MethodHandles$Lookup;",
322+
false);
323+
324+
// Load the Owner class
325+
mv.visitLdcInsn(Type.getObjectType(Type.getInternalName(method.getDeclaringClass())));
326+
327+
// Load the Method Name
328+
mv.visitLdcInsn(method.getName());
329+
330+
// Create Class[] array
331+
Class<?> argTypes[] = method.getParameterTypes();
332+
pushInt(mv, (short) argTypes.length);
333+
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class");
334+
335+
// Load the specific Class objects and store in array
336+
for (short i = 0; i < argTypes.length; i++) {
337+
mv.visitInsn(Opcodes.DUP);
338+
pushInt(mv, i);
339+
loadClassConstant(mv, argTypes[i]);
340+
mv.visitInsn(Opcodes.AASTORE);
341+
}
342+
343+
// Invoke getDeclaredMethod
344+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
345+
"java/lang/Class",
346+
"getDeclaredMethod",
347+
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;",
348+
false);
349+
350+
// Invoke method.setAccessible(true)
351+
mv.visitInsn(Opcodes.DUP);
352+
mv.visitInsn(Opcodes.ICONST_1);
353+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
354+
"java/lang/reflect/Method",
355+
"setAccessible",
356+
"(Z)V",
357+
false);
358+
359+
// Invoke Lookup.unresolve(method)
360+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
361+
"java/lang/invoke/MethodHandles$Lookup",
362+
"unreflect",
363+
"(Ljava/lang/reflect/Method;)Ljava/lang/invoke/MethodHandle;",
364+
false);
365+
366+
// Return result
367+
mv.visitInsn(Opcodes.ARETURN);
368+
369+
mv.visitMaxs(0, 0);
370+
mv.visitEnd();
371+
}
372+
373+
private void generateMethodOverride(ClassWriter cw, Method method, String internalClassName, String internalParentName) {
265374
boolean hasJsonValueReturn = !hasLegacyVaadin() && JsonValue.class.isAssignableFrom(method.getReturnType());
266375
boolean hasJsonValueParams = !hasLegacyVaadin() && hasJsonValueParameters(method);
267376

@@ -275,6 +384,14 @@ private void generateMethodOverride(ClassWriter cw, Method method, String intern
275384
mv.visitAnnotation(Type.getDescriptor(ClientCallable.class), true);
276385
mv.visitCode();
277386

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

@@ -301,9 +418,24 @@ private void generateMethodOverride(ClassWriter cw, Method method, String intern
301418
}
302419
}
303420

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

308440
if (hasJsonValueReturn) {
309441
// Store result in local variable
@@ -339,6 +471,42 @@ private void generateMethodOverride(ClassWriter cw, Method method, String intern
339471
mv.visitEnd();
340472
}
341473

474+
private void pushInt(MethodVisitor mv, short value) {
475+
if (value >= -1 && value <= 5) {
476+
mv.visitInsn(Opcodes.ICONST_0 + value);
477+
} else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
478+
mv.visitIntInsn(Opcodes.BIPUSH, value);
479+
} else {
480+
mv.visitIntInsn(Opcodes.SIPUSH, value);
481+
}
482+
}
483+
484+
private void loadClassConstant(MethodVisitor mv, Class<?> clazz) {
485+
if (clazz.isPrimitive()) {
486+
if (clazz == int.class) {
487+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;");
488+
} else if (clazz == boolean.class) {
489+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;");
490+
} else if (clazz == byte.class) {
491+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;");
492+
} else if (clazz == char.class) {
493+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;");
494+
} else if (clazz == short.class) {
495+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;");
496+
} else if (clazz == float.class) {
497+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;");
498+
} else if (clazz == long.class) {
499+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;");
500+
} else if (clazz == double.class) {
501+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;");
502+
} else {
503+
throw new IllegalArgumentException("Unsupported type: " + clazz);
504+
}
505+
} else {
506+
mv.visitLdcInsn(Type.getType(clazz));
507+
}
508+
}
509+
342510
private int loadParameter(MethodVisitor mv, Class<?> paramType, int localVarIndex) {
343511
if (!paramType.isPrimitive()) {
344512
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)