DefaultExpressionEvaluationContext.java

/*
 * Copyright 2017-2022 original authors
 *
 * 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
 *
 * https://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.
 */
package io.micronaut.expressions.context;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ConstructorElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.PropertyElement;
import io.micronaut.inject.ast.PropertyElementQuery;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static io.micronaut.inject.ast.ElementQuery.ALL_METHODS;
import static java.util.function.Predicate.not;

/**
 * Default implementation of {@link ExtensibleExpressionEvaluationContext}. Extending
 * this context will always return new instance instead of modifying the existing one.
 *
 * @since 4.0.0
 * @author Sergey Gavrilov
 */
@Internal
public class DefaultExpressionEvaluationContext implements ExtensibleExpressionEvaluationContext {

    private final Collection<ClassElement> classElements;
    private final MethodElement methodElement;

    private final ClassElement thisType;

    DefaultExpressionEvaluationContext(ClassElement... classElements) {
        this(null, null, classElements);
    }

    private DefaultExpressionEvaluationContext(ClassElement thisType,
                                               MethodElement methodElement,
                                               ClassElement... classElements) {
        this.thisType = thisType;
        this.methodElement = methodElement;
        this.classElements = Arrays.asList(classElements);
    }

    @Override
    public ExtensibleExpressionEvaluationContext withThis(ClassElement classElement) {
        return new DefaultExpressionEvaluationContext(
            classElement,
            methodElement,
            classElements.toArray(ClassElement[]::new)
        );
    }

    @Override
    public DefaultExpressionEvaluationContext extendWith(MethodElement methodElement) {
        ClassElement resolvedThis = methodElement.isStatic() || methodElement instanceof ConstructorElement ? null : methodElement.getOwningType();
        return new DefaultExpressionEvaluationContext(
            resolvedThis,
            methodElement,
            classElements.toArray(ClassElement[]::new)
        );
    }

    @Override
    public DefaultExpressionEvaluationContext extendWith(ClassElement classElement) {
        return new DefaultExpressionEvaluationContext(
            this.thisType,
            this.methodElement,
            ArrayUtils.concat(classElements.toArray(ClassElement[]::new), classElement)
        );
    }

    @Override
    public ClassElement findThis() {
        return thisType;
    }

    @Override
    public List<MethodElement> findMethods(String name) {
        return classElements.stream()
                   .flatMap(element -> findMatchingMethods(element, name).stream())
                   .toList();
    }

    private List<MethodElement> findMatchingMethods(ClassElement classElement, String name) {
        String propertyName = NameUtils.getPropertyNameForGetter(name,
            PropertyElementQuery.of(classElement.getAnnotationMetadata())
                .getReadPrefixes());

        return Stream.concat(
                classElement.getEnclosedElements(ALL_METHODS.onlyAccessible().named(name)).stream(),
                getNamedProperties(classElement, propertyName).stream()
                    .map(PropertyElement::getReadMethod)
                    .flatMap(Optional::stream))
                   .distinct()
                   .filter(method -> method.getSimpleName().equals(name))
                   .toList();
    }

    @Override
    public List<PropertyElement> findProperties(String name) {
        return classElements.stream()
                   .flatMap(classElement -> getNamedProperties(classElement, name).stream())
                   .toList();
    }

    @Override
    public List<ParameterElement> findParameters(String name) {
        if (this.methodElement == null) {
            return Collections.emptyList();
        }

        return Arrays.stream(methodElement.getParameters())
                   .filter(parameter -> parameter.getName().equals(name))
                   .toList();
    }

    private List<PropertyElement> getNamedProperties(ClassElement classElement, String name) {
        return classElement.getBeanProperties(
                PropertyElementQuery.of(classElement.getAnnotationMetadata())
                    .includes(Collections.singleton(name)))
                   .stream()
                   .filter(not(PropertyElement::isExcluded))
                   .toList();
    }
}