diff --git a/pom.xml b/pom.xml
index 1df303d7..d0841c39 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,11 @@
com.github.ben-manes.caffeine
caffeine
+
+ org.freemarker
+ freemarker
+ 2.3.32
+
org.apache.poi
poi
diff --git a/src/main/java/cn/lihongjie/coal/base/dto/CommonQuery.java b/src/main/java/cn/lihongjie/coal/base/dto/CommonQuery.java
index d6862b9f..e3052a4e 100644
--- a/src/main/java/cn/lihongjie/coal/base/dto/CommonQuery.java
+++ b/src/main/java/cn/lihongjie/coal/base/dto/CommonQuery.java
@@ -149,6 +149,13 @@ public class CommonQuery {
parseKey(root, x.key), c.convert(x.value, String.class));
});
+ map.put(
+ Tuple.of("eq", Boolean.class),
+ (Root root, CriteriaBuilder criteriaBuilder, QueryItem x, ConversionService c) -> {
+ return criteriaBuilder.equal(
+ parseKey(root, x.key), c.convert(x.value, Boolean.class));
+ });
+
map.put(
Tuple.of("eq", Integer.class),
(Root root, CriteriaBuilder criteriaBuilder, QueryItem x, ConversionService c) -> {
@@ -207,6 +214,12 @@ public class CommonQuery {
return criteriaBuilder.notEqual(
parseKey(root, x.key), c.convert(x.value, String.class));
});
+ map.put(
+ Tuple.of("neq", Boolean.class),
+ (Root root, CriteriaBuilder criteriaBuilder, QueryItem x, ConversionService c) -> {
+ return criteriaBuilder.notEqual(
+ parseKey(root, x.key), c.convert(x.value, Boolean.class));
+ });
map.put(
Tuple.of("neq", Integer.class),
diff --git a/src/main/java/cn/lihongjie/coal/common/FreeMakerUtils.java b/src/main/java/cn/lihongjie/coal/common/FreeMakerUtils.java
new file mode 100644
index 00000000..28ae1f9b
--- /dev/null
+++ b/src/main/java/cn/lihongjie/coal/common/FreeMakerUtils.java
@@ -0,0 +1,338 @@
+package cn.lihongjie.coal.common;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.google.common.base.Splitter;
+
+import freemarker.core.Environment;
+import freemarker.template.*;
+import freemarker.template.utility.DeepUnwrap;
+
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.DigestUtils;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.regex.Pattern;
+
+@UtilityClass
+public class FreeMakerUtils {
+
+ private static final Configuration cfg;
+
+ private static final Cache templateCache =
+ Caffeine.newBuilder().maximumSize(10_000).build();
+
+ static {
+
+ // Create your Configuration instance, and specify if up to what FreeMarker
+ // version (here 2.3.32) do you want to apply the fixes that are not 100%
+ // backward-compatible. See the Configuration JavaDoc for details.
+ cfg = new Configuration(Configuration.VERSION_2_3_32);
+
+ // Specify the source where the template files come from. Here I set a
+ // plain directory for it, but non-file-system sources are possible too:
+
+ // From here we will set the settings recommended for new projects. These
+ // aren't the defaults for backward compatibilty.
+
+ // Set the preferred charset template files are stored in. UTF-8 is
+ // a good choice in most applications:
+ cfg.setDefaultEncoding("UTF-8");
+
+ // Sets how errors will appear.
+ // During web page *development* TemplateExceptionHandler.HTML_DEBUG_HANDLER is better.
+ cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+
+ // Don't log exceptions inside FreeMarker that it will thrown at you anyway:
+ cfg.setLogTemplateExceptions(false);
+
+ // Wrap unchecked exceptions thrown during template processing into TemplateException-s:
+ cfg.setWrapUncheckedExceptions(true);
+
+ // Do not fall back to higher scopes when reading a null loop variable:
+ cfg.setFallbackOnNullLoopVariable(false);
+
+ // To accomodate to how JDBC returns values; see Javadoc!
+ cfg.setSQLDateAndTimeTimeZone(TimeZone.getDefault());
+
+ cfg.setNumberFormat("c");
+
+ cfg.setSharedVariable("if", new MyBatisIf());
+ cfg.setSharedVariable("trim", new MyBatisTrim());
+ cfg.setSharedVariable("where", new MyBatisTrim("where", "AND |OR "));
+ cfg.setSharedVariable("set", new MyBatisTrim("set", ","));
+ cfg.setSharedVariable("foreach", new MyBatisForeach());
+ }
+
+ private static String getString(Map params, String key) throws TemplateModelException {
+ return getString(params, key, true, null);
+ }
+
+ private static String getString(Map params, String key, String defaultVal)
+ throws TemplateModelException {
+ return getString(params, key, false, defaultVal);
+ }
+
+ private static String getString(Map params, String key, boolean required, String defaultVal)
+ throws TemplateModelException {
+
+ Object o = params.get(key);
+
+ if (o == null && required) {
+ throw new TemplateModelException(key + " is required");
+ }
+
+ if (o == null) {
+ return defaultVal;
+ }
+
+ if (!(o instanceof SimpleScalar s)) {
+ throw new TemplateModelException(key + " must be string");
+ }
+ return StringUtils.defaultIfEmpty(s.getAsString(), defaultVal);
+ }
+
+ private static Boolean getBoolean(Map params, String key, boolean required, Boolean defaultVal)
+ throws TemplateModelException {
+
+ Object o = params.get(key);
+
+ if (o == null && required) {
+ throw new TemplateModelException(key + " is required");
+ }
+
+ if (o == null) {
+ return defaultVal;
+ }
+
+ if ((o instanceof SimpleScalar s)) {
+ return Boolean.parseBoolean(s.getAsString());
+ } else if (o instanceof TemplateBooleanModel s) {
+ return s.getAsBoolean();
+ } else {
+
+ throw new TemplateModelException(key + " must be boolean");
+ }
+ }
+
+ @SneakyThrows
+ public static String render( String template, Object model) {
+
+ if (StringUtils.isBlank(template)) {
+ return "";
+ }
+
+ if (model == null) {
+ model = new HashMap<>();
+ }
+
+ String hex = DigestUtils.md5DigestAsHex(template.getBytes(StandardCharsets.UTF_8));
+ Template ft =
+ templateCache
+ .asMap()
+ .computeIfAbsent(
+ hex,
+ k -> {
+ try {
+ return new Template(hex, template, cfg);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ StringWriter out = new StringWriter();
+ ft.process(model, out);
+
+ return out.toString();
+ }
+
+ public static class MyBatisIf implements TemplateDirectiveModel {
+
+ @Override
+ public void execute(
+ Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+ throws TemplateException, IOException {
+
+ Object o = params.get("test");
+ if (o == null) {
+ throw new TemplateException("test is required", env);
+ }
+
+ if (o instanceof TemplateBooleanModel b) {
+
+ if (b.getAsBoolean() && body != null) {
+ body.render(env.getOut());
+ }
+ } else {
+ throw new TemplateException("test must be boolean", env);
+ }
+ }
+ }
+
+ /**
+ * #{item}
+ */
+ public static class MyBatisForeach implements TemplateDirectiveModel {
+
+ @Override
+ public void execute(
+ Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+ throws TemplateException, IOException {
+
+ String item = getString(params, "item", "__item");
+ String index = getString(params, "index", "__index");
+ String open = getString(params, "open", "");
+ String separator = getString(params, "separator", "");
+ String close = getString(params, "close", "");
+ Boolean nullable = getBoolean(params, "nullable", false, true);
+
+ Object collection = params.get("collection");
+
+ if (nullable && collection == null) {
+ return;
+ }
+
+ if (collection == null) {
+ throw new TemplateException("collection is null and nullable set to false", env);
+ }
+
+ Object object = DeepUnwrap.permissiveUnwrap((TemplateModel) collection);
+
+
+
+ if (object instanceof String){
+ object =
+ DeepUnwrap.permissiveUnwrap(
+ env.getDataModelOrSharedVariable((String) object));
+
+ }else if (object instanceof TemplateModelAdapter adapter){
+ object = DeepUnwrap.unwrap(adapter.getTemplateModel());
+ }
+
+
+ env.getOut().write(open);
+
+ if (object instanceof Iterable> iterable) {
+
+ int idx = 0;
+ for (Object o : iterable) {
+
+ env.setVariable(item, env.getObjectWrapper().wrap(o));
+ env.setVariable(index, env.getObjectWrapper().wrap(idx));
+ idx++;
+
+ body.render(env.getOut());
+
+ env.getOut().write(separator);
+ }
+
+ } else if (object instanceof Map, ?> map) {
+
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ env.setVariable(item, env.getObjectWrapper().wrap(entry.getValue()));
+ env.setVariable(index, env.getObjectWrapper().wrap(entry.getKey()));
+
+ body.render(env.getOut());
+
+ env.getOut().write(separator);
+ }
+
+ } else if (object.getClass().isArray()) {
+
+ Object[] array = (Object[]) object;
+
+ for (int i = 0; i < array.length; i++) {
+ env.setVariable(item, env.getObjectWrapper().wrap(array[i]));
+ env.setVariable(index, env.getObjectWrapper().wrap(i));
+
+ body.render(env.getOut());
+
+ env.getOut().write(separator);
+ }
+
+ } else {
+ throw new TemplateException("collection must be iterable/map/array", env);
+ }
+
+ env.getOut().write(close);
+ }
+ }
+
+ public static class MyBatisTrim implements TemplateDirectiveModel {
+
+ private SimpleScalar defaultPrefix;
+
+ private SimpleScalar defaultPrefixOverrides;
+
+ public MyBatisTrim() {}
+
+ public MyBatisTrim(String defaultPrefix, String defaultPrefixOverrides) {
+
+ Assert.hasLength(defaultPrefix, "defaultPrefix is required");
+ Assert.hasLength(defaultPrefixOverrides, "defaultPrefixOverrides is required");
+
+ this.defaultPrefix = new SimpleScalar(defaultPrefix);
+ this.defaultPrefixOverrides = new SimpleScalar(defaultPrefixOverrides);
+ }
+
+ @Override
+ public void execute(
+ Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+ throws TemplateException, IOException {
+
+ Object p = ObjectUtils.defaultIfNull(params.get("prefix"), defaultPrefix);
+ Object po =
+ ObjectUtils.defaultIfNull(
+ params.get("prefixOverrides"), defaultPrefixOverrides);
+
+ try {
+
+ Assert.isInstanceOf(SimpleScalar.class, p, "prefix must be string");
+ Assert.isInstanceOf(SimpleScalar.class, po, "prefixOverrides must be string");
+
+ String prefix = ((SimpleScalar) p).getAsString();
+ String prefixOverrides = ((SimpleScalar) po).getAsString();
+
+ if (body != null) {
+ StringWriter out = new StringWriter();
+ body.render(out);
+
+ String bodyStr = out.toString();
+
+ if (StringUtils.isNotBlank(bodyStr)) {
+
+ Iterable iterable =
+ Splitter.on("|").omitEmptyStrings().split(prefixOverrides);
+
+ for (String s : iterable) {
+
+ bodyStr =
+ RegExUtils.replaceFirst(
+ bodyStr,
+ Pattern.compile(
+ "^(\\s*)" + Pattern.quote(s), CASE_INSENSITIVE),
+ "$1");
+ }
+ }
+ bodyStr = prefix + " " + bodyStr;
+
+ env.getOut().write(bodyStr);
+ }
+
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), e, env);
+ }
+ }
+ }
+}
diff --git a/src/main/java/cn/lihongjie/coal/common/JpaUtils.java b/src/main/java/cn/lihongjie/coal/common/JpaUtils.java
index dd8764e6..fa57d5f3 100644
--- a/src/main/java/cn/lihongjie/coal/common/JpaUtils.java
+++ b/src/main/java/cn/lihongjie/coal/common/JpaUtils.java
@@ -1,19 +1,33 @@
package cn.lihongjie.coal.common;
+import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;
import com.google.common.base.CaseFormat;
-import jakarta.persistence.Tuple;
-import jakarta.persistence.TupleElement;
+import jakarta.persistence.*;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.formula.functions.T;
import org.springframework.core.convert.ConversionService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.support.PageableUtils;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -26,11 +40,15 @@ public class JpaUtils {
(String name) -> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
public static List