Engine.java

package pro.verron.officestamper.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import pro.verron.officestamper.api.DocxPart;
import pro.verron.officestamper.api.ExceptionResolver;
import pro.verron.officestamper.api.Insert;
import pro.verron.officestamper.api.ProcessorContext;

import java.util.Objects;

/// The core engine of OfficeStamper, responsible for processing expressions.
public class Engine {
    private static final Logger log = LoggerFactory.getLogger(Engine.class);

    private final SpelParserConfiguration parserConfiguration;
    private final ExceptionResolver exceptionResolver;
    private final ObjectResolverRegistry objectResolverRegistry;
    private final String expression;
    private final DocxPart docxPart;
    private final SpelExpressionParser expressionParser;

    /// Constructs an Engine.
    ///
    /// @param parserConfiguration the parser configuration.
    /// @param exceptionResolver the exception resolver.
    /// @param objectResolverRegistry the object resolver registry.
    /// @param processorContext the processor context.
    public Engine(
            SpelParserConfiguration parserConfiguration,
            ExceptionResolver exceptionResolver,
            ObjectResolverRegistry objectResolverRegistry,
            ProcessorContext processorContext
    ) {
        this.parserConfiguration = parserConfiguration;
        this.expressionParser = new SpelExpressionParser(parserConfiguration);
        this.exceptionResolver = exceptionResolver;
        this.objectResolverRegistry = objectResolverRegistry;
        this.expression = processorContext.expression();
        this.docxPart = processorContext.part();
    }

    /// Processes the provided evaluation context against the expression defined in the processor context.
    ///
    /// The method attempts to resolve an expression using the given evaluation context.
    ///
    /// If successful, the process completes and logs a debug message.
    ///
    /// Otherwise, on failure ([SpelEvaluationException] or [SpelParseException]), it handles the exception by invoking
    /// the exceptionResolver and logs an error.
    ///
    /// @param evaluationContext the evaluation context for processing the expression.
    ///
    /// @return true if the processing was successful, otherwise false
    public boolean process(EvaluationContext evaluationContext) {
        SpelNode spelNode;
        try {
            spelNode = parseAST(expressionParser, expression);
        } catch (SpelParseException e) {
            var msgTemplate = "Expression %s could not be parsed successfully.";
            var message = msgTemplate.formatted(expression, evaluationContext);
            exceptionResolver.resolve(expression, message, e);
            return false;
        }

        var expressionState = buildExpressionState(evaluationContext);
        try {
            spelNode.getValue(expressionState);
            log.debug("Processed '{}' successfully.", expression);
        } catch (SpelEvaluationException e) {
            var msgTemplate = "Expression %s could not be processed against context '%s'";
            var message = msgTemplate.formatted(expression, evaluationContext);
            exceptionResolver.resolve(expression, message, e);
            return false;
        }

        return true;
    }

    private static SpelNode parseAST(SpelExpressionParser parser, String expression) {
        var parsedExpression = parser.parseRaw(expression);
        log.trace("Parsed '{}' successfully.", expression);
        return parsedExpression.getAST();
    }

    private ExpressionState buildExpressionState(EvaluationContext evaluationContext) {
        var contextBranchTypedValue = evaluationContext.getRootObject();
        var contextBranch = (ContextBranch) Objects.requireNonNull(contextBranchTypedValue.getValue());
        var rootObject = contextBranch.root();
        var rootObjectTypedValue = new TypedValue(rootObject);
        var expressionState = new ExpressionState(evaluationContext, rootObjectTypedValue, parserConfiguration);
        for (Object o : contextBranch) {
            expressionState.pushActiveContextObject(new TypedValue(o));
            expressionState.enterScope();
        }
        return expressionState;
    }

    /// Resolves an [Insert] object by processing the provided evaluation context using the current processor context.
    /// Combines the processor context's part and expression with various resolvers to achieve the resolution.
    ///
    /// @param evaluationContext the evaluation context for processing the expression.
    ///
    /// @return an [Insert] object representing the resolved result of the expression within the context.
    public Insert resolve(EvaluationContext evaluationContext) {
        SpelNode spelNode;
        try {
            spelNode = parseAST(expressionParser, expression);
        } catch (SpelParseException e) {
            var msgTemplate = "Expression %s could not be parsed successfully.";
            var message = msgTemplate.formatted(expression, evaluationContext);
            return exceptionResolver.resolve(expression, message, e);
        }

        var expressionState = buildExpressionState(evaluationContext);
        Object javaResolution;
        try {
            javaResolution = spelNode.getValue(expressionState);
            log.debug("Resolved '{}' successfully.", expression);
        } catch (SpelEvaluationException e) {
            var msgTemplate = "Expression %s could not be resolved against context '%s'";
            var message = msgTemplate.formatted(expression, evaluationContext);
            return exceptionResolver.resolve(expression, message, e);
        }

        try {
            var docxResolution = objectResolverRegistry.resolve(docxPart, expression, javaResolution);
            log.debug("Converted '{}' to docx ({}) successfully.", expression, docxResolution);
            return docxResolution;
        } catch (SpelEvaluationException e) {
            var msgTemplate = "Expression %s could not be converted to docx inserts.";
            var message = msgTemplate.formatted(expression, evaluationContext);
            return exceptionResolver.resolve(expression, message, e);
        }
    }
}