Skip to content

Commit

Permalink
Remove StandardMethodResolver and refactor method resolution
Browse files Browse the repository at this point in the history
Eliminated `StandardMethodResolver.java` and adapted the method resolution logic by creating a more modular and reusable structure with `Invokers` and `ReflectionExecutor`. Updated `DocxStamper` to integrate the new mechanism, simplifying the handling of method executors.
  • Loading branch information
caring-coder committed Sep 23, 2024
1 parent f36cf62 commit c5da6ac
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class CommentProcessorRegistry {

private static final Logger logger = LoggerFactory.getLogger(CommentProcessorRegistry.class);
private final DocxPart source;
private final Map<Class<?>, Object> commentProcessors;
private final Map<Class<?>, ?> commentProcessors;
private final ExpressionResolver expressionResolver;
private final ExceptionResolver exceptionResolver;

Expand All @@ -51,7 +51,7 @@ public class CommentProcessorRegistry {
public CommentProcessorRegistry(
DocxPart source,
ExpressionResolver expressionResolver,
Map<Class<?>, Object> commentProcessors,
Map<Class<?>, ?> commentProcessors,
ExceptionResolver exceptionResolver
) {
this.source = source;
Expand Down
38 changes: 20 additions & 18 deletions engine/src/main/java/pro/verron/officestamper/core/DocxStamper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.relationships.Namespaces;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
Expand All @@ -19,6 +17,8 @@
import java.util.Map;
import java.util.function.Function;

import static pro.verron.officestamper.core.Invokers.streamInvokers;

/**
* The DocxStamper class is an implementation of the {@link OfficeStamper}
* interface that is used to stamp DOCX templates with a context object and
Expand All @@ -32,8 +32,6 @@
public class DocxStamper
implements OfficeStamper<WordprocessingMLPackage> {

private static final Logger logger = LoggerFactory.getLogger(DocxStamper.class);

private final List<PreProcessor> preprocessors;
private final PlaceholderReplacer placeholderReplacer;
private final Function<DocxPart, CommentProcessorRegistry> commentProcessorRegistrySupplier;
Expand Down Expand Up @@ -65,30 +63,22 @@ private DocxStamper(
SpelParserConfiguration spelParserConfiguration,
ExceptionResolver exceptionResolver
) {
var commentProcessors = new HashMap<Class<?>, Object>();

var methodResolver = new StandardMethodResolver(commentProcessors, expressionFunctions);
var expressionParser = new SpelExpressionParser(spelParserConfiguration);

var evaluationContext = new StandardEvaluationContext();
evaluationContextConfigurer.configureEvaluationContext(evaluationContext);
evaluationContext.addMethodResolver(methodResolver);

var expressionParser = new SpelExpressionParser(spelParserConfiguration);
var expressionResolver = new ExpressionResolver(evaluationContext, expressionParser);

var typeResolverRegistry = new ObjectResolverRegistry(resolvers);

this.placeholderReplacer = new PlaceholderReplacer(typeResolverRegistry,
this.placeholderReplacer = new PlaceholderReplacer(
typeResolverRegistry,
expressionResolver,
Placeholders.raw(lineBreakPlaceholder),
exceptionResolver);

for (var entry : configurationCommentProcessors.entrySet()) {
Class<?> aClass = entry.getKey();
Function<ParagraphPlaceholderReplacer, CommentProcessor> processorFunction = entry.getValue();
CommentProcessor value = processorFunction.apply(placeholderReplacer);
commentProcessors.put(aClass, value);
}
var commentProcessors = buildCommentProcessors(configurationCommentProcessors);
evaluationContext.addMethodResolver(new Invokers(streamInvokers(commentProcessors)));
evaluationContext.addMethodResolver(new Invokers(streamInvokers(expressionFunctions)));

this.commentProcessorRegistrySupplier = source -> new CommentProcessorRegistry(source,
expressionResolver,
Expand All @@ -98,6 +88,18 @@ private DocxStamper(
this.preprocessors = new ArrayList<>(preprocessors);
}

private Map<Class<?>, CommentProcessor> buildCommentProcessors(Map<Class<?>,
Function<ParagraphPlaceholderReplacer, CommentProcessor>> commentProcessors) {
var processors = new HashMap<Class<?>, CommentProcessor>();
for (var entry : commentProcessors.entrySet()) {
processors.put(
entry.getKey(),
entry.getValue()
.apply(placeholderReplacer));
}
return processors;
}

/**
* {@inheritDoc}
*
Expand Down
44 changes: 17 additions & 27 deletions engine/src/main/java/pro/verron/officestamper/core/Invoker.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
package pro.verron.officestamper.core;

import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.TypedValue;
import org.springframework.lang.NonNull;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import static java.util.Arrays.asList;


/**
* The Invoker class encapsulates an object and a method to be invoked on that object. It provides a way to execute
* The Invoker class encapsulates an object and a method to be invoked on that object. It provides a way to
* execute
* the specified method with given arguments within a certain evaluation context.
*
* @param object the target object on which the method is to be invoked
* @param method the method to be invoked on the target object
*/
public record Invoker(Object object, Method method)
implements MethodExecutor {
@Override @NonNull
public TypedValue execute(
@NonNull EvaluationContext context,
@NonNull Object target,
@NonNull Object... arguments
)
throws AccessException {
try {
var value = method.invoke(object, arguments);
return new TypedValue(value);
} catch (InvocationTargetException | IllegalAccessException e) {
var message = "Failed to invoke method %s with arguments [%s] from object %s"
.formatted(method, Arrays.toString(arguments), object);
throw new AccessException(message, e);
}
public record Invoker(String name, Invokers.Args args, MethodExecutor executor) {

/**
* @param obj the target object on which the method is to be invoked
* @param method the method to be invoked on the target object
*/
public Invoker(Object obj, Method method) {
this(method.getName(), asList(method.getParameterTypes()), new ReflectionExecutor(obj, method));
}

public Invoker(String name, List<Class<?>> args, MethodExecutor executor) {
this(name, new Invokers.Args(args), executor);
}
}
91 changes: 91 additions & 0 deletions engine/src/main/java/pro/verron/officestamper/core/Invokers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package pro.verron.officestamper.core;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.lang.NonNull;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Stream;

import static java.util.Arrays.stream;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;

/**
* Resolves methods that are used as expression functions or comment processors.
*
* @author Joseph Verron
* @version ${version}
* @since 1.6.2
*/
public class Invokers
implements MethodResolver {
private final Map<String, Map<Args, MethodExecutor>> map;

public Invokers(Stream<Invoker> invokerStream) {
map = invokerStream.collect(groupingBy(
Invoker::name,
toMap(Invoker::args, Invoker::executor)
));
}

static Stream<Invoker> streamInvokers(Map<Class<?>, ?> interfaces2implementations) {
return interfaces2implementations
.entrySet()
.stream()
.flatMap(Invokers::streamInvokers);
}

private static Stream<Invoker> streamInvokers(Entry<Class<?>, ?> interface2implementation) {
return streamInvokers(interface2implementation.getKey(), interface2implementation.getValue());
}

private static Stream<Invoker> streamInvokers(Class<?> key, Object obj) {
return stream(key.getDeclaredMethods())
.map(method -> new Invoker(obj, method));
}

/** {@inheritDoc} */
@Override
public MethodExecutor resolve(
@NonNull EvaluationContext context,
@NonNull Object targetObject,
@NonNull String name,
@NonNull List<TypeDescriptor> argumentTypes
) {
List<Class<?>> argumentClasses = new ArrayList<>();
argumentTypes.forEach(at -> argumentClasses.add(at.getType()));
return map.getOrDefault(name, emptyMap())
.entrySet()
.stream()
.filter(entry -> entry.getKey()
.validate(argumentClasses))
.map(Entry::getValue)
.findFirst()
.orElse(null);
}

public record Args(List<Class<?>> sourceTypes) {
public boolean validate(List<Class<?>> searchedTypes) {
if (searchedTypes.size() != sourceTypes.size())
return false;

var sourceTypesQ = new ArrayDeque<>(sourceTypes);
var searchedTypesQ = new ArrayDeque<>(searchedTypes);
var valid = true;
while (!sourceTypesQ.isEmpty() && valid) {
Class<?> parameterType = sourceTypesQ.remove();
Class<?> searchedType = searchedTypesQ.remove();
valid = parameterType.isAssignableFrom(searchedType);
}
return valid;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package pro.verron.officestamper.core;

import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.TypedValue;
import org.springframework.lang.NonNull;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

public record ReflectionExecutor(Object object, Method method)
implements MethodExecutor {

@Override @NonNull
public TypedValue execute(
@NonNull EvaluationContext context,
@NonNull Object target,
@NonNull Object... arguments
)
throws AccessException {
try {
var value = method.invoke(object, arguments);
return new TypedValue(value);
} catch (InvocationTargetException | IllegalAccessException e) {
var message = "Failed to invoke method %s with arguments [%s] from object %s"
.formatted(method, Arrays.toString(arguments), object);
throw new AccessException(message, e);
}
}
}

This file was deleted.

0 comments on commit c5da6ac

Please sign in to comment.