All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.icthh.xm.commons.lep.groovy.annotation.LepServiceTransformation Maven / Gradle / Ivy

There is a newer version: 4.0.20
Show newest version
package com.icthh.xm.commons.lep.groovy.annotation;

import com.icthh.xm.commons.lep.api.BaseLepContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.similarity.LevenshteinDistance;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.builder.AstBuilder;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.transform.AbstractASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Arrays.stream;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toSet;
import static org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS;
import static org.codehaus.groovy.control.CompilePhase.CANONICALIZATION;

@Slf4j
@GroovyASTTransformation(phase = CANONICALIZATION)
public class LepServiceTransformation extends AbstractASTTransformation {

    private volatile static Map> LEP_CONTEXT_FIELDS;
    private volatile static Set LEP_CONTEXT_TYPE_HIERARCHY; // all classes and interfaces in hierarchy
    private volatile static Set LEP_CONTEXT_CLASS_HIERARCHY; // all classes to BaseLepContext

    public static final String LEP_CONTEXT_NAME = "lepContext";

    public static void init(Class lepContextClass) {
        Set> typeHierarchy = buildTypeHierarchy(lepContextClass);
        Set> classHierarchy = typeHierarchy.stream()
            .filter(BaseLepContext.class::isAssignableFrom)
            .collect(toSet());


        Map> fields = new HashMap<>();
        classHierarchy.forEach(type -> getFields(fields, "", type));
        String canonicalName = lepContextClass.getCanonicalName();
        if (canonicalName != null) {
            fields.put(canonicalName, List.of("with{it}"));
        }

        typeHierarchy.forEach(it -> fields.putIfAbsent(it.getCanonicalName(), List.of("with{it}")));

        LEP_CONTEXT_CLASS_HIERARCHY = Set.copyOf(toCanonicalNames(classHierarchy));
        LEP_CONTEXT_TYPE_HIERARCHY = Set.copyOf(toCanonicalNames(typeHierarchy));
        LEP_CONTEXT_FIELDS = Map.copyOf(fields);
    }

    private static Set toCanonicalNames(Set> typeHierarchy) {
        return typeHierarchy.stream().map(Class::getCanonicalName).collect(Collectors.toSet());
    }

    private static Set> buildTypeHierarchy(Class lepContextClass) {
        Set> typeHierarchy = new HashSet<>();
        Class currentClass = lepContextClass;
        while (currentClass != null) {
            typeHierarchy.add(currentClass);
            typeHierarchy.addAll(Arrays.asList(currentClass.getInterfaces()));
            currentClass = currentClass.getSuperclass();
        }
        return typeHierarchy;
    }

    private static void getFields(Map> fields, String basePath, Class type) {
        Set> memberClasses = new HashSet<>(List.of(type.getNestMembers()));
        stream(type.getFields())
                .filter(field -> !field.getType().isAssignableFrom(Object.class))
                .filter(field -> !memberClasses.contains(field.getType()))
                .forEach(field -> {
                    fields.putIfAbsent(field.getType().getCanonicalName(), new ArrayList<>());
                    fields.get(field.getType().getCanonicalName()).add(basePath + field.getName());
                });
        stream(type.getFields())
                .filter(field -> memberClasses.contains(field.getType()))
                .forEach(field -> {
                    getFields(fields, field.getName() + ".", field.getType());
                });
    }

    @Override
    public void visit(ASTNode[] nodes, SourceUnit source) {
        try {
            internalVisit(nodes, source);
        } catch (Throwable e) {
            log.error("Error during LepServiceTransformation", e);
            throw e;
        }
    }

    private void internalVisit(ASTNode[] nodes, SourceUnit source) {
        ClassNode classNode = (ClassNode) nodes[1];

        boolean isLepServiceFactoryEnabled = isLepServiceFactoryEnabled(nodes);
        List statements = generateFieldAssignment(classNode, isLepServiceFactoryEnabled);
        var constructor = findExistingLepConstructor(classNode);

        String mockClass = "\n class A { public A(def lepContext) {  } }";
        var ast = new AstBuilder().buildFromString(CANONICALIZATION, true, mockClass);
        ConstructorNode generatedConstructor = ((ClassNode) ast.get(1)).getDeclaredConstructors().get(0);
        generatedConstructor.setCode(new BlockStatement(statements, new VariableScope()));

        if (constructor.isPresent() && constructor.get().getParameters().length > 0) {
            ConstructorNode original = constructor.get();
            var constructorStatements = new ArrayList(statements);
            constructorStatements.add(original.getCode());
            original.setCode(new BlockStatement(constructorStatements, new VariableScope()));
        } else if (constructor.isPresent() && constructor.get().getParameters().length == 0) {
            ConstructorNode original = constructor.get();
            var constructorStatements = new ArrayList(statements);
            constructorStatements.add(original.getCode());
            generatedConstructor.setCode(new BlockStatement(constructorStatements, new VariableScope()));
            classNode.removeConstructor(constructor.get());
            classNode.addConstructor(generatedConstructor);
        } else {
            classNode.addConstructor(generatedConstructor);
        }

    }

    private Statement createAssignment(String fieldName, String rhsExpression) {
        Expression thisExpression = new VariableExpression("this");
        Expression fieldExpression = new PropertyExpression(thisExpression, fieldName);
        Expression rhsExpr = createExpression(rhsExpression);

        return new ExpressionStatement(new BinaryExpression(
            fieldExpression,
            Token.newSymbol("=", -1, -1),
            rhsExpr
        ));
    }

    private Statement createServiceInstance(FieldNode field) {
        Expression thisExpression = new VariableExpression("this");
        Expression fieldExpression = new PropertyExpression(thisExpression, field.getName());

        List constructors = field.getType().getDeclaredConstructors();

        if (constructors.stream().anyMatch(this::isLepContextConstructor) || isAnnotated(field.getType(), LepConstructor.class)) {
            return callLepConstructor(field, fieldExpression);
        }

        if (constructors.isEmpty() || hasEmptyArgumentConstructor(field.getType())) {
            return new ExpressionStatement(new BinaryExpression(
                fieldExpression,
                Token.newSymbol("=", -1, -1),
                new ConstructorCallExpression(field.getType(), EMPTY_ARGUMENTS)
            ));
        }

        log.error("No suitable class constructor found for field {}", field.getName());
        return callLepConstructor(field, fieldExpression);
    }

    private static ExpressionStatement callLepConstructor(FieldNode field, Expression fieldExpression) {
        Expression classConstructorCall = new ConstructorCallExpression(
            field.getType(),
            new ArgumentListExpression(new VariableExpression(LEP_CONTEXT_NAME))
        );

        return new ExpressionStatement(new BinaryExpression(
            fieldExpression,
            Token.newSymbol("=", -1, -1),
            classConstructorCall
        ));
    }

    private static boolean hasEmptyArgumentConstructor(ClassNode type) {
        return type.getDeclaredConstructors().stream().anyMatch(it -> it.getParameters().length == 0);
    }

    private Expression createExpression(String expression) {
        String[] parts = expression.split("\\.");
        Expression result = new VariableExpression(parts[0]);
        for (int i = 1; i < parts.length; i++) {
            result = new PropertyExpression(result, parts[i]);
        }
        return result;
    }

    private boolean isLepServiceFactoryEnabled(ASTNode[] nodes) {
        AnnotationNode annotationNode = null;
        for (ASTNode node : nodes) {
            if (node instanceof AnnotationNode && ((AnnotationNode) node).getClassNode().getName().equals(
                    LepConstructor.class.getCanonicalName()
            )) {
                annotationNode = (AnnotationNode) node;
                break;
            }
        }

        if (annotationNode != null) {
            // Get the boolean attribute value by name
            var expression = annotationNode.getMember("useLepFactory");
            if (expression instanceof ConstantExpression) {
                return Boolean.parseBoolean(((ConstantExpression) expression).getValue().toString());
            }
        }
        return true;
    }

    private List generateFieldAssignment(ClassNode classNode, boolean isLepServiceFactoryEnabled) {
        List statements = new ArrayList<>();
        classNode.getFields().forEach(field -> {
            if (LEP_CONTEXT_CLASS_HIERARCHY.contains(field.getType().getName()) || lepContextConventionField(field)) {
                statements.add(createAssignment(field.getName(), LEP_CONTEXT_NAME));
            } else if (canBeInjected(field) && isInLepContext(field)) {
                generateFieldFromLepContextAssigment(statements, field);
            } else if (canBeInjected(field) && isLepService(field)) {
                generateLepServiceCreations(statements, classNode, field, isLepServiceFactoryEnabled);
            }
        });
        return statements;
    }

    private static boolean lepContextConventionField(FieldNode field) {
        return field.getName().equals(LEP_CONTEXT_NAME) && (field.isDynamicTyped() || LEP_CONTEXT_TYPE_HIERARCHY.contains(field.getType().getName()));
    }

    private static boolean canBeInjected(FieldNode field) {
        return !field.isStatic() && (field.isFinal() || isAnnotated(field, LepInject.class)) && !isAnnotated(field, LepIgnoreInject.class);
    }

    private static boolean isAnnotated(AnnotatedNode node, Class annotationClass) {
        return node.getAnnotations().stream().anyMatch(it -> it.getClassNode().getName().equals(annotationClass.getCanonicalName()));
    }

    private void generateLepServiceCreations(List statements, ClassNode classNode, FieldNode field, boolean isLepServiceFactoryEnabled) {

        this.addImportToClass(classNode, field);

        if (isLepServiceFactoryEnabled) {
            statements.add(createServiceInstanceUsingGetInstance(field));
        } else {
            statements.add(createServiceInstance(field));
        }
    }

    private Statement createServiceInstanceUsingGetInstance(FieldNode field) {
        Expression thisExpression = new VariableExpression("this");
        Expression fieldExpression = new PropertyExpression(thisExpression, field.getName());
        Expression lepContextExpression = new VariableExpression(LEP_CONTEXT_NAME);
        Expression lepServicesExpression = new PropertyExpression(lepContextExpression, "lepServices");

        Expression getInstanceMethodCall = new MethodCallExpression(
            lepServicesExpression,
            "getInstance",
            new ArgumentListExpression(new ClassExpression(field.getType()))
        );

        return new ExpressionStatement(new BinaryExpression(
            fieldExpression,
            Token.newSymbol("=", -1, -1),
            getInstanceMethodCall
        ));
    }

    private void addImportToClass(ClassNode classNode, FieldNode field) {
        ModuleNode moduleNode = classNode.getModule();
        if (moduleNode == null) {
            return;
        }

        // Check if the import already exists
        String typeName = field.getType().getName();
        boolean importExists = moduleNode.getImports().stream()
            .anyMatch(importNode -> typeName.equals(importNode.getClassName()));
        if (importExists) {
            return;
        }

        moduleNode.addImport(typeName, field.getType());
    }

    private void generateFieldFromLepContextAssigment(List statements, FieldNode field) {
        String lepContextFieldName = LEP_CONTEXT_NAME + "." + getFieldName(field);
        statements.add(createAssignment(field.getName(), lepContextFieldName));
    }

    private Optional findExistingLepConstructor(ClassNode classNode) {
        return classNode.getDeclaredConstructors().stream().filter(it -> {
            boolean isNeededConstructor = false;
            if (it.getParameters().length == 0) {
                isNeededConstructor = true;
            } else if (it.getParameters().length == 1) {
                Parameter parameter = it.getParameters()[0];
                isNeededConstructor = parameter.getName().equals(LEP_CONTEXT_NAME) && isLepContextConstructor(it);
            }
            if (!isNeededConstructor) {
                log.warn("Constructor {} is not suitable for LepConstructor annotation. " +
                    "This must be constructor with exactly one argument with name \"lepContext\"", it);
            }
            return isNeededConstructor;
        }).findFirst();
    }

    private boolean isLepContextConstructor(ConstructorNode constructorNode) {
        if (constructorNode.getParameters().length == 1) {
            Parameter parameter = constructorNode.getParameters()[0];
            ClassNode parameterType = parameter.getType();
            return LEP_CONTEXT_TYPE_HIERARCHY.contains(parameterType.getName());
        }
        return false;
    }

    private boolean isLepService(FieldNode field) {
        return isAnnotated(field.getType(), LepConstructor.class) || isAnnotated(field.getType(), LepInjectableService.class);
    }

    private boolean isInLepContext(FieldNode field) {
        return LEP_CONTEXT_FIELDS.containsKey(field.getType().getName());
    }

    private String getFieldName(FieldNode field) {
        List candidates = LEP_CONTEXT_FIELDS.get(field.getType().getName());
        String lepContextFieldName = candidates.get(0);
        if (candidates.size() > 1) {
            LevenshteinDistance levenshteinDistance = new LevenshteinDistance();
            lepContextFieldName = candidates.stream().min(comparing(name ->
                levenshteinDistance.apply(name.toLowerCase(), field.getName().toLowerCase())
            )).get();
        }
        return lepContextFieldName;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy