Skip to content

Commit

Permalink
Add custom function support to SpEL resolver
Browse files Browse the repository at this point in the history
Allow adding custom functions to SpEL resolver by name, parameter types, and lambda implementation. Simplifies adding functions without a full interface and implementing class.
  • Loading branch information
caring-coder committed Oct 31, 2024
1 parent 03e8ff5 commit e438ddc
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 163 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pro.verron.officestamper.api;

import pro.verron.officestamper.utils.TriFunction;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

public record CustomFunction(
String name,
List<Class<?>> parameterTypes,
Function<List<Object>, Object> function
) {
public interface NeedsFunctionImpl<T> {
void withImplementation(Function<T, ?> function);
}

public interface NeedsBiFunctionImpl<T, U> {
void withImplementation(BiFunction<T, U, ?> object);
}

public interface NeedsTriFunctionImpl<T, U, V> {
void withImplementation(TriFunction<T, U, V, ?> function);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package pro.verron.officestamper.api;

import org.springframework.expression.spel.SpelParserConfiguration;
import pro.verron.officestamper.api.CustomFunction.NeedsBiFunctionImpl;
import pro.verron.officestamper.api.CustomFunction.NeedsFunctionImpl;
import pro.verron.officestamper.api.CustomFunction.NeedsTriFunctionImpl;

import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -32,6 +35,7 @@ public interface OfficeStamperConfiguration {
* @param failOnUnresolvedExpression flag indicating whether to fail on unresolved expressions
*
* @return the updated OfficeStamperConfiguration object
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand All @@ -45,6 +49,7 @@ public interface OfficeStamperConfiguration {
* Determines whether to leave empty on expression error.
*
* @return true if expression errors are left empty, false otherwise
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand All @@ -57,6 +62,7 @@ public interface OfficeStamperConfiguration {
* Determines whether unresolved expressions in the OfficeStamper configuration should be replaced.
*
* @return true if unresolved expressions should be replaced, false otherwise.
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand All @@ -69,6 +75,7 @@ public interface OfficeStamperConfiguration {
* Retrieves the default value for unresolved expressions.
*
* @return the default value for unresolved expressions
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand All @@ -83,6 +90,7 @@ public interface OfficeStamperConfiguration {
* @param unresolvedExpressionsDefaultValue the default value for unresolved expressions
*
* @return the updated OfficeStamperConfiguration object
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand All @@ -98,6 +106,7 @@ public interface OfficeStamperConfiguration {
* @param replaceUnresolvedExpressions flag indicating whether to replace unresolved expressions
*
* @return the updated OfficeStamperConfiguration object
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand All @@ -115,6 +124,7 @@ OfficeStamperConfiguration replaceUnresolvedExpressions(
* @param leaveEmpty boolean value indicating whether to leave empty on expression error
*
* @return the updated OfficeStamperConfiguration object
*
* @deprecated This method is deprecated because it offers limited functionality by just checking a flag.
* It is replaced by {@link #setExceptionResolver(ExceptionResolver)} , which provides
* complete customization over the behavior during resolution failures. The new method
Expand Down Expand Up @@ -262,4 +272,17 @@ OfficeStamperConfiguration setSpelParserConfiguration(
ExceptionResolver getExceptionResolver();

OfficeStamperConfiguration setExceptionResolver(ExceptionResolver exceptionResolver);

List<CustomFunction> customFunctions();

<T> NeedsFunctionImpl<T> addCustomFunction(String name, Class<T> class0);

<T, U> NeedsBiFunctionImpl<T, U> addCustomFunction(String name, Class<T> class0, Class<U> class1);

<T, U, V> NeedsTriFunctionImpl<T, U, V> addCustomFunction(
String name,
Class<T> class0,
Class<U> class1,
Class<V> class2
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.relationships.Namespaces;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
Expand All @@ -11,10 +12,7 @@

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Function;

import static pro.verron.officestamper.core.Invokers.streamInvokers;
Expand Down Expand Up @@ -46,6 +44,7 @@ public DocxStamper(OfficeStamperConfiguration configuration) {
configuration.getLineBreakPlaceholder(),
configuration.getEvaluationContextConfigurer(),
configuration.getExpressionFunctions(),
configuration.customFunctions(),
configuration.getResolvers(),
configuration.getCommentProcessors(),
configuration.getPreprocessors(),
Expand All @@ -57,6 +56,7 @@ private DocxStamper(
@NonNull String lineBreakPlaceholder,
EvaluationContextConfigurer evaluationContextConfigurer,
Map<Class<?>, Object> expressionFunctions,
List<CustomFunction> functions,
List<ObjectResolver> resolvers,
Map<Class<?>, Function<ParagraphPlaceholderReplacer, CommentProcessor>> configurationCommentProcessors,
List<PreProcessor> preprocessors,
Expand All @@ -79,6 +79,9 @@ private DocxStamper(
var commentProcessors = buildCommentProcessors(configurationCommentProcessors);
evaluationContext.addMethodResolver(new Invokers(streamInvokers(commentProcessors)));
evaluationContext.addMethodResolver(new Invokers(streamInvokers(expressionFunctions)));
evaluationContext.addMethodResolver(new Invokers(functions.stream().map(cf -> new Invoker(cf.name(),
new Invokers.Args(cf.parameterTypes()),
(context, target, arguments) -> new TypedValue(cf.function().apply(Arrays.asList(arguments)))))));

this.commentProcessorRegistrySupplier = source -> new CommentProcessorRegistry(
source,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package pro.verron.officestamper.core;


import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.lang.NonNull;
import pro.verron.officestamper.api.*;
import pro.verron.officestamper.api.CustomFunction.NeedsBiFunctionImpl;
import pro.verron.officestamper.api.CustomFunction.NeedsFunctionImpl;
import pro.verron.officestamper.core.functions.BiFunctionBuilder;
import pro.verron.officestamper.core.functions.FunctionBuilder;
import pro.verron.officestamper.core.functions.TriFunctionBuilder;
import pro.verron.officestamper.preset.CommentProcessorFactory;
import pro.verron.officestamper.preset.EvaluationContextConfigurers;
import pro.verron.officestamper.preset.ExceptionResolvers;
Expand Down Expand Up @@ -32,6 +38,7 @@ public class DocxStamperConfiguration
private final List<ObjectResolver> resolvers = new ArrayList<>();
private final Map<Class<?>, Object> expressionFunctions = new HashMap<>();
private final List<PreProcessor> preprocessors = new ArrayList<>();
private final List<CustomFunction> functions = new ArrayList<>();
private String lineBreakPlaceholder = "\n";
private EvaluationContextConfigurer evaluationContextConfigurer = EvaluationContextConfigurers.defaultConfigurer();
private boolean failOnUnresolvedExpression = true;
Expand Down Expand Up @@ -387,5 +394,27 @@ public DocxStamperConfiguration replaceUnresolvedExpressions(boolean replaceUnre
return this;
}

public void addCustomFunction(CustomFunction function) {
this.functions.add(function);
}

@Override public List<CustomFunction> customFunctions() {
return functions;
}


@Override public <T> NeedsFunctionImpl<T> addCustomFunction(String name, Class<T> class0) {
return new FunctionBuilder<>(this, name, class0);
}

@Override public <T, U> NeedsBiFunctionImpl<T, U> addCustomFunction(String name, Class<T> class0, Class<U> class1) {
return new BiFunctionBuilder<>(this, name, class0, class1);
}

@Override public <T, U, V> CustomFunction.NeedsTriFunctionImpl<T, U, V> addCustomFunction(
String name,
Class<T> class0, Class<U> class1, Class<V> class2
) {
return new TriFunctionBuilder<>(this, name, class0, class1,class2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
public record Invoker(String name, Invokers.Args args, MethodExecutor executor) {

/**
* @param obj the target object on which the method is to be invoked
* @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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
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.expression.*;
import org.springframework.lang.NonNull;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pro.verron.officestamper.core.functions;

import pro.verron.officestamper.api.CustomFunction;
import pro.verron.officestamper.core.DocxStamperConfiguration;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

public class BiFunctionBuilder<T, U>
implements CustomFunction.NeedsBiFunctionImpl<T, U> {
private final DocxStamperConfiguration source;
private final String name;
private final Class<T> class0;
private final Class<U> class1;

public BiFunctionBuilder(DocxStamperConfiguration source, String name, Class<T> class0, Class<U> class1) {
this.source = source;
this.name = name;
this.class0 = class0;
this.class1 = class1;
}

@Override public void withImplementation(BiFunction<T, U, ?> implementation) {
Function<List<Object>, Object> function = args -> {
var arg0 = class0.cast(args.getFirst());
var arg1 = class1.cast(args.get(1));
return implementation.apply(arg0, arg1);
};
var customFunction = new CustomFunction(name, List.of(class0, class1), function);
source.addCustomFunction(customFunction);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pro.verron.officestamper.core.functions;

import pro.verron.officestamper.api.CustomFunction;
import pro.verron.officestamper.core.DocxStamperConfiguration;

import java.util.List;
import java.util.function.Function;

public class FunctionBuilder<T>
implements CustomFunction.NeedsFunctionImpl<T> {
private final DocxStamperConfiguration source;
private final String name;
private final Class<T> class0;

public FunctionBuilder(DocxStamperConfiguration source, String name, Class<T> class0) {
this.source = source;
this.name = name;
this.class0 = class0;
}

@Override public void withImplementation(Function<T, ?> implementation) {
Function<List<Object>, Object> objectFunction = args -> {
var arg0 = class0.cast(args.getFirst());
return implementation.apply(arg0);
};
var customFunction = new CustomFunction(name, List.of(class0), objectFunction);
source.addCustomFunction(customFunction);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package pro.verron.officestamper.core.functions;

import pro.verron.officestamper.api.CustomFunction;
import pro.verron.officestamper.core.DocxStamperConfiguration;
import pro.verron.officestamper.utils.TriFunction;

import java.util.List;
import java.util.function.Function;

public class TriFunctionBuilder<T, U, V>
implements CustomFunction.NeedsTriFunctionImpl<T, U, V> {
private final DocxStamperConfiguration source;
private final String name;
private final Class<T> class0;
private final Class<U> class1;
private final Class<V> class2;

public TriFunctionBuilder(
DocxStamperConfiguration source, String name, Class<T> class0, Class<U> class1, Class<V> class2
) {
this.source = source;
this.name = name;
this.class0 = class0;
this.class1 = class1;
this.class2 = class2;
}

@Override public void withImplementation(TriFunction<T, U, V, ?> implementation) {
Function<List<Object>, Object> function = args -> {
var arg0 = class0.cast(args.getFirst());
var arg1 = class1.cast(args.get(1));
var arg2 = class2.cast(args.get(2));
return implementation.apply(arg0, arg1, arg2);
};
var customFunction = new CustomFunction(name, List.of(class0, class1, class2), function);
source.addCustomFunction(customFunction);
}
}

This file was deleted.

Loading

0 comments on commit e438ddc

Please sign in to comment.