migration

This commit is contained in:
hdvt
2025-11-03 21:16:02 +03:00
parent 6093465f0d
commit 15f77a945a
56 changed files with 991 additions and 604 deletions

View File

@@ -0,0 +1,20 @@
plugins {
id 'java'
}
group = 'com.github.hdvtdev'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'com.google.auto.service:auto-service:1.1.1'
implementation project(':core')
implementation 'org.ow2.asm:asm:9.9'
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
//implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.3'
implementation project(":event-handlers-annotations")
}

View File

@@ -0,0 +1,172 @@
import com.google.auto.service.AutoService;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.*;
import java.util.stream.Collectors;
import hdvtdev.telegram.handler.annotations.*;
@AutoService(Processor.class)
@SupportedAnnotationTypes({
"hdvtdev.telegram.handler.annotations.OnChosenInlineResult",
"hdvtdev.telegram.handler.annotations.OnChatMemberUpdated",
"hdvtdev.telegram.handler.annotations.OnChatBoostRemoved",
"hdvtdev.telegram.handler.annotations.OnPreCheckoutQuery",
"hdvtdev.telegram.handler.annotations.OnBusinessConnection",
"hdvtdev.telegram.handler.annotations.OnPaidMediaPurchased",
"hdvtdev.telegram.handler.annotations.OnUpdate",
"hdvtdev.telegram.handler.annotations.BotCommand",
"hdvtdev.telegram.handler.annotations.OnPoll",
"hdvtdev.telegram.handler.annotations.OnMessage",
"hdvtdev.telegram.handler.annotations.OnChatJoinRequest",
"hdvtdev.telegram.handler.annotations.OnChatBoostUpdated",
"hdvtdev.telegram.handler.annotations.OnCallbackQuery",
"hdvtdev.telegram.handler.annotations.OnShippingQuery",
"hdvtdev.telegram.handler.annotations.OnInlineQuery",
"hdvtdev.telegram.handler.annotations.OnPollAnswer"})
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class EventHandlersProcessor extends AbstractProcessor {
private final StringBuilder sb = new StringBuilder();
private static final String PACKAGE = "hdvtdev.telegram.handler.annotations.";
private static final String HANDLER_PACKAGE = "hdvtdev.telegram.handler.";
private static final Set<String> SUPPORTED_ANNOTATIONS = Set.of(
OnChosenInlineResult.class,
OnChatMemberUpdated.class,
OnChatBoostRemoved.class,
OnPreCheckoutQuery.class,
OnBusinessConnection.class,
OnPaidMediaPurchased.class,
OnUpdate.class,
OnPoll.class,
OnMessage.class,
OnChatJoinRequest.class,
OnChatBoostUpdated.class,
OnCallbackQuery.class,
OnShippingQuery.class,
OnInlineQuery.class,
OnPollAnswer.class
).stream().map(Class::getName).collect(Collectors.toSet());
private static final Map<String, String> SUPPORTED_PARAMETERS = Map.ofEntries(
Map.entry("Update", "hdvtdev.telegram.core.objects.Update"),
Map.entry("InlineQuery", "hdvtdev.telegram.core.objects.InlineQuery"),
Map.entry("ChatMemberUpdated", "hdvtdev.telegram.core.objects.chat.ChatMemberUpdated"),
Map.entry("ChatBoostRemoved", "hdvtdev.telegram.core.objects.chatboost.ChatBoostRemoved"),
Map.entry("PreCheckoutQuery", "hdvtdev.telegram.core.objects.payment.PreCheckoutQuery"),
Map.entry("BusinessConnection", "hdvtdev.telegram.core.objects.business.BusinessConnection"),
Map.entry("PaidMediaPurchased", "hdvtdev.telegram.core.objects.media.paidmedia.PaidMediaPurchased"),
Map.entry("Poll", "hdvtdev.telegram.core.objects.poll.Poll"),
Map.entry("Message", "hdvtdev.telegram.core.objects.Message"),
Map.entry("ChatJoinRequest", "hdvtdev.telegram.core.objects.chat.ChatJoinRequest"),
Map.entry("ChatBoostUpdated", "hdvtdev.telegram.core.objects.chatboost.ChatBoostUpdated"),
Map.entry("CallbackQuery", "hdvtdev.telegram.core.objects.callback.CallbackQuery"),
Map.entry("ShippingQuery", "hdvtdev.telegram.core.objects.payment.ShippingQuery"),
Map.entry("PollAnswer", "hdvtdev.telegram.core.objects.poll.PollAnswer"),
Map.entry("ChosenInlineResult", "hdvtdev.telegram.core.objects.ChosenInlineResult")
);
public static String getFullParameterName(String simpleName) {
return SUPPORTED_PARAMETERS.get(simpleName);
}
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printNote("Called");
for (Element element : roundEnvironment.getRootElements()) {
for (Element enclosedElement : element.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.METHOD) {
processMethod((ExecutableElement) enclosedElement);
}
}
}
if (roundEnvironment.processingOver()) {
write();
}
return true;
}
private void write() {
}
private void processMethod(ExecutableElement methodElement) {
String methodName = methodElement.getSimpleName().toString();
List<AnnotationMirror> foundAnnotations = new ArrayList<>();
for (AnnotationMirror mirror : methodElement.getAnnotationMirrors()) {
String mirrorAnnotationName = Util.getAnnotationMirrorName(mirror);
if (SUPPORTED_ANNOTATIONS.contains(PACKAGE + mirrorAnnotationName)) {
foundAnnotations.add(mirror);
}
}
if (foundAnnotations.size() > 1) {
List<String> annotationNames = new ArrayList<>();
for (AnnotationMirror mirror : foundAnnotations) {
annotationNames.add("@" + Util.getAnnotationMirrorName(mirror));
}
messager.printError(String.format("Method %s cannot have multiple mutually exclusive annotations. Found: %s. Please leave only one.",
methodName, String.join(", ", annotationNames)), methodElement);
return;
}
if (foundAnnotations.size() == 1) {
AnnotationMirror annotation = foundAnnotations.getFirst();
String annotationSimpleName = Util.getAnnotationMirrorName(annotation);
var params = methodElement.getParameters();
if (params.isEmpty()) {
messager.printError(String.format("Method %s, annotated with @%s, must have at least one parameter.",
methodName, annotationSimpleName), methodElement);
return;
}
String firstParameterType = Util.getParameterClassName(params.getFirst().asType());
String requiredParameter = Util.checkParameter(annotationSimpleName, firstParameterType);
if (!requiredParameter.isEmpty()) {
messager.printError(String.format("Incorrect parameter type for method %s. Annotation @%s requires %s, but found %s.",
methodName, annotationSimpleName, requiredParameter, firstParameterType), params.getFirst());
return;
}
if (!annotationSimpleName.equals("BotCommand")) {
String paramName = params.getFirst().getSimpleName().toString();
Set<String> filters = new HashSet<>();
AnnotationValue vv = Util.getAnnotationValue(annotation, "filters");
@SuppressWarnings("unchecked")
List<? extends AnnotationValue> enumValues = vv == null ? List.of() : (List<? extends AnnotationValue>) vv.getValue();
for (AnnotationValue v : enumValues) {
if (v.getValue() instanceof VariableElement enumConstant) {
filters.add(enumConstant.getSimpleName().toString());
}
}
sb.append(Generator.generateVoid(methodName, String.format("%s %s", firstParameterType, paramName),
Generator.generateFilterBlock(filters, paramName)));
}
}
}
}

View File

@@ -0,0 +1,42 @@
import java.util.Set;
public final class Generator {
private Generator() {
}
public static String generateVoid(String name, String obj, String block) {
return String.format("""
public static void %s(%s) {
%s
}
""", name, obj, block);
}
public static String generateFilterBlock(Set<String> filters, String objName) {
StringBuilder sb = new StringBuilder();
for (String filter : filters) {
boolean skip = true;
sb.append("if (");
sb.append(objName);
sb.append(".");
for (String part : filter.split("_")) {
part = part.toLowerCase();
if (skip) {
sb.append(part);
skip = false;
} else {
char[] chars = part.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
sb.append(chars);
}
}
sb.append("()) {");
sb.append("\n /* some code */ \n}\n");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,108 @@
import org.objectweb.asm.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class HandlerWriter {
public static void write(String body) throws IOException {
String classFilePath = "models/Handlers.class"; // Путь к вашему файлу
Path path = Paths.get(classFilePath);
// 1. Читаем исходный класс в байты
byte[] originalBytecode = Files.readAllBytes(path);
// 2. Инициализируем ASM
ClassReader classReader = new ClassReader(originalBytecode);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 3. Создаем наш визитор, который найдет и заменит метод
ClassVisitor classVisitor = new MethodReplacer(classWriter);
// 4. Запускаем процесс
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
// 5. Получаем измененный байт-код
byte[] modifiedBytecode = classWriter.toByteArray();
// 6. Перезаписываем исходный файл
Files.write(path, modifiedBytecode);
System.out.println("Метод invoke в файле " + classFilePath + " был успешно заменен.");
}
private static class MethodReplacer extends ClassVisitor {
public MethodReplacer(ClassVisitor classVisitor) {
super(Opcodes.ASM9, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
String targetDescriptor = "(L" + EventHandlersProcessor.getFullParameterName("Update").replace('.', '/') +
";)V";
if ("invoke".equals(name) && targetDescriptor.equals(descriptor)) {
System.out.println("Найден метод 'invoke'. Заменяем его тело...");
// Найден нужный метод. Не передаем его дальше (старое тело удаляется).
// Вместо этого создаем новый MethodVisitor для генерации нового тела.
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
generateNewBody(mv);
// Возвращаем null, так как мы уже создали метод через cv.visitMethod
return null;
}
// Для всех остальных методов - просто передаем их дальше без изменений
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
private void generateNewBody(MethodVisitor mv) {
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello from the new invoke method!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// Код для: Integer updateId = update.getUpdateId();
// `update` - это первый аргумент, поэтому он в локальной переменной 1 (0 это `this`)
mv.visitVarInsn(Opcodes.ALOAD, 1);
// Предполагаем, что getUpdateId() возвращает Integer. Если он возвращает int, нужно использовать valueOf.
// Для примера, пусть он возвращает Integer.
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, EventHandlersProcessor.getFullParameterName("Update"),
"getUpdateId",
"()Ljava/lang/Long;", false);
// Сохраняем результат в локальную переменную 2
mv.visitVarInsn(Opcodes.ASTORE, 2);
// Код для: System.out.println("Update ID: " + updateId);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// Создаем новый StringBuilder для конкатенации строк
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("Update ID: ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// Загружаем updateId из локальной переменной 2
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// Завершаем метод (поскольку он void, используем RETURN)
mv.visitInsn(Opcodes.RETURN);
// Указываем максимальный размер стека и количество локальных переменных.
// ASM может вычислить это за нас, если использовать ClassWriter.COMPUTE_FRAMES
mv.visitMaxs(0, 0); // Значения игнорируются при COMPUTE_FRAMES
mv.visitEnd();
}
}
}

View File

@@ -0,0 +1,63 @@
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import java.util.Map;
public final class Util {
private Util() {
}
public static String checkParameter(String simpleAnnotationName, String simpleParameterName) {
String reqParameter = switch (simpleAnnotationName) {
case "OnMessage", "BotCommand" -> "Message";
case "OnBusinessConnection" -> "BusinessConnection";
case "OnCallbackQuery" -> "CallbackQuery";
case "OnChatBoostRemoved" -> "ChatBoostRemoved";
case "OnChatBoostUpdated" -> "ChatBoostUpdated";
case "OnChatJoinRequest" -> "ChatJoinRequest";
case "OnChatMemberUpdated" -> "ChatMemberUpdated";
case "OnChosenInlineResult" -> "ChosenInlineResult";
case "OnInlineQuery" -> "InlineQuery";
case "OnPaidMediaPurchased" -> "PaidMediaPurchased";
case "OnPoll" -> "Poll";
case "OnPollAnswer" -> "PollAnswer";
case "OnPreCheckoutQuery" -> "PreCheckoutQuery";
case "OnShippingQuery" -> "ShippingQuery";
case "OnUpdate" -> "Update";
default -> throw new IllegalStateException("Unexpected value: " + simpleAnnotationName);
};
return reqParameter.equals(simpleParameterName) ? "" : reqParameter;
}
public static String getParameterClassName(TypeMirror typeMirror) {
String methodParameterName = "unknown (probably primitive type)";
if (typeMirror instanceof DeclaredType declaredType) {
Element typeElement = declaredType.asElement();
methodParameterName = typeElement.getSimpleName().toString();
}
return methodParameterName;
}
public static String getAnnotationMirrorName(AnnotationMirror mirror) {
return mirror.getAnnotationType().asElement().getSimpleName().toString();
}
public static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String attributeName) {
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
if (entry.getKey().getSimpleName().toString().equals(attributeName)) {
return entry.getValue();
}
}
return null;
}
}