001package cn.sticki.validator.spel;
002
003import cn.sticki.validator.spel.exception.SpelNotSupportedTypeException;
004import cn.sticki.validator.spel.exception.SpelValidException;
005import cn.sticki.validator.spel.parse.SpelParser;
006import cn.sticki.validator.spel.result.FieldValidResult;
007import cn.sticki.validator.spel.result.ObjectValidResult;
008import lombok.extern.slf4j.Slf4j;
009import org.jetbrains.annotations.NotNull;
010
011import java.lang.annotation.Annotation;
012import java.lang.reflect.Field;
013import java.lang.reflect.Method;
014import java.util.ArrayList;
015import java.util.HashSet;
016import java.util.List;
017import java.util.Set;
018import java.util.concurrent.ConcurrentHashMap;
019
020/**
021 * spel 相关注解的执行器,对使用了 {@link SpelConstraint} 进行标记的注解执行校验。
022 *
023 * @author 阿杆
024 * @version 1.0
025 * @see SpelConstraint
026 * @since 2024/4/29
027 */
028@Slf4j
029public class SpelValidExecutor {
030
031        private static final String MESSAGE = "message";
032
033        private static final String CONDITION = "condition";
034
035        private static final String GROUP = "group";
036
037        /**
038         * 验证对象
039         * <p>
040         * 如果对象中有任意使用了 spel 约束注解的字段,则会对该字段进行校验。
041         *
042         * @param verifiedObject 被校验的对象
043         * @param validateGroups 分组信息,只有同组的注解才会被校验
044         * @return 对象校验结果
045         */
046        @NotNull
047        public static ObjectValidResult validateObject(@NotNull Object verifiedObject, @NotNull Set<Object> validateGroups) {
048                log.debug("Spel validate start, class [{}], groups [{}]", verifiedObject.getClass().getName(), validateGroups);
049
050                List<FieldValidResult> validationResults = new ArrayList<>();
051
052                // 获取类的字段 todo 缓存
053                Field[] fields = verifiedObject.getClass().getDeclaredFields();
054                for (Field field : fields) {
055                        field.setAccessible(true);
056                        validateField(verifiedObject, field, validateGroups, validationResults);
057                }
058
059                ObjectValidResult validResult = new ObjectValidResult();
060                validResult.addFieldResults(validationResults);
061
062                log.debug("Spel validate over, result {}", validResult.getErrors());
063                return validResult;
064        }
065
066        /**
067         * 验证字段
068         */
069        private static void validateField(
070                        @NotNull Object verifiedObject,
071                        @NotNull Field verifiedField,
072                        @NotNull Set<Object> validGroups,
073                        @NotNull List<FieldValidResult> validationResults
074        ) {
075                // 获取字段上的注解 todo 缓存
076                Annotation[] annotations = verifiedField.getAnnotations();
077                for (Annotation originalAnno : annotations) {
078                        String annoName = originalAnno.annotationType().getName();
079                        if (annoName.endsWith("$List") || annoName.endsWith("Container")) {
080                                // 容器注解,需要获取容器内部的注解类型,其声明类为真实的注解类
081                                Class<?> clazz = originalAnno.annotationType().getDeclaringClass();
082                                //noinspection unchecked
083                                Annotation[] originalAnnoArray = verifiedField.getAnnotationsByType((Class<Annotation>) clazz);
084                                for (Annotation anno : originalAnnoArray) {
085                                        // 执行验证
086                                        validateFieldAnnotation(anno, verifiedObject, verifiedField, validationResults, validGroups);
087                                }
088                        } else {
089                                // 执行验证
090                                validateFieldAnnotation(originalAnno, verifiedObject, verifiedField, validationResults, validGroups);
091                        }
092                }
093        }
094
095        /**
096         * 对任意的注解执行校验,当注解不是Ex约束注解时,将返回null。
097         *
098         * @param annotation        注解数组,数组内的注解必须为同一类型
099         * @param verifiedObject    被校验的对象
100         * @param verifiedField     被校验的字段,必须存在于被校验的对象中
101         * @param validationResults 校验结果列表
102         * @param validateGroups    分组信息
103         */
104        private static void validateFieldAnnotation(
105                        @NotNull Annotation annotation,
106                        @NotNull Object verifiedObject,
107                        @NotNull Field verifiedField,
108                        @NotNull List<FieldValidResult> validationResults,
109                        @NotNull Set<Object> validateGroups
110        ) {
111                Class<? extends Annotation> annoClazz = annotation.annotationType();
112                // 验证注解的合法性
113                if (!isSpelConstraintAnnotationCached(annoClazz)) {
114                        return;
115                }
116
117                log.debug("===> Find target annotation [{}], verifiedField [{}], anno param[{}]", annoClazz.getSimpleName(), verifiedField.getName(), annotation);
118
119                // 获取验证器实例 todo 缓存
120                Class<? extends SpelConstraintValidator<?>> validatorClass = annoClazz.getAnnotation(SpelConstraint.class).validatedBy();
121                SpelConstraintValidator<? extends Annotation> validator = getValidatorInstance(validatorClass);
122
123                // 判断对象的类型是否受支持
124                Set<Class<?>> supported = validator.supportType();
125                Class<?> verifiedObjectClass = verifiedObject.getClass();
126                if (supported.stream().noneMatch(clazz -> clazz.isInstance(verifiedObjectClass))) {
127                        log.error("===> Object type not supported, skip validate. supported types [{}]", supported);
128                        throw new SpelNotSupportedTypeException(verifiedObjectClass, supported);
129                }
130
131                // 匹配分组
132                Set<Object> annoGroups = parseGroups(annotation, verifiedObject);
133                if (!annoGroups.isEmpty() && !matchGroup(validateGroups, annoGroups)) {
134                        log.debug("===> Group not matched, skip validate. annotation groups [{}]", annoGroups);
135                        return;
136                }
137
138                // 判断condition条件是否成立
139                String condition = getAnnotationValue(annotation, CONDITION);
140                if (!condition.isEmpty() && !SpelParser.parse(condition, verifiedObject, Boolean.class)) {
141                        log.debug("===> Condition not valid, skip validate. condition [{}]", condition);
142                        return;
143                }
144
145                // 执行校验
146                FieldValidResult validationResult = doValidate(validator, annotation, verifiedObject, verifiedField);
147                validationResults.add(validationResult);
148
149                log.debug("===> Validate result [{}]", validationResult.isSuccess());
150        }
151
152        /**
153         * 执行校验
154         *
155         * @param validator      校验器
156         * @param annotation     注解
157         * @param verifiedObject 被校验的对象
158         * @param verifiedField  被校验的字段,必须存在于被校验的对象中
159         */
160        @NotNull
161        private static <A extends Annotation> FieldValidResult doValidate(
162                        @NotNull SpelConstraintValidator<?> validator,
163                        @NotNull A annotation,
164                        @NotNull Object verifiedObject,
165                        @NotNull Field verifiedField
166        ) {
167                FieldValidResult result;
168                try {
169                        // noinspection unchecked
170                        result = ((SpelConstraintValidator<A>) validator).isValid(annotation, verifiedObject, verifiedField);
171                } catch (RuntimeException e) {
172                        log.error("spel validate error: {} ;Located in the annotation [{}] of class [{}] field [{}]",
173                                        e.getMessage(), annotation.annotationType().getName(), verifiedObject.getClass().getName(), verifiedField.getName());
174                        throw e;
175                }
176                autoFillValidResult(result, annotation, verifiedField);
177                return result;
178        }
179
180        /**
181         * 约束注解缓存
182         */
183        private static final ConcurrentHashMap<Class<? extends Annotation>, Boolean> CONSTRAINT_ANNOTATION_CACHE = new ConcurrentHashMap<>();
184
185        /**
186         * 判断注解是否为合法的约束注解,该方法会缓存结果。
187         */
188        private static boolean isSpelConstraintAnnotationCached(@NotNull Class<? extends Annotation> annotationType) {
189                return CONSTRAINT_ANNOTATION_CACHE.computeIfAbsent(annotationType, SpelValidExecutor::isSpelConstraintAnnotation);
190        }
191
192        /**
193         * 判断注解是否为合法的约束注解
194         */
195        private static boolean isSpelConstraintAnnotation(@NotNull Class<? extends Annotation> annotationType) {
196                if (!annotationType.isAnnotationPresent(SpelConstraint.class)) {
197                        return false;
198                }
199
200                if (GetMethod.action(annotationType, MESSAGE).run() == null) {
201                        log.warn("The annotation [{}] must have a method named [message] that returns a string.", annotationType.getName());
202                        return false;
203                }
204
205                if (GetMethod.action(annotationType, CONDITION).run() == null) {
206                        log.warn("The annotation [{}] must have a method named [condition] that returns a string.", annotationType.getName());
207                        return false;
208                }
209
210                if (GetMethod.action(annotationType, GROUP).run() == null) {
211                        log.warn("The annotation [{}] must have a method named [group] that returns a Array<String>.", annotationType.getName());
212                        return false;
213                }
214
215                return true;
216        }
217
218        /**
219         * 判断组别是否匹配
220         */
221        @SuppressWarnings("RedundantIfStatement")
222        private static boolean matchGroup(@NotNull Set<Object> validateGroups, @NotNull Set<Object> annoGroups) {
223                if (validateGroups.isEmpty()) {
224                        return true;
225                }
226                if (validateGroups.stream().anyMatch(annoGroups::contains)) {
227                        return true;
228                }
229                return false;
230        }
231
232        /**
233         * 解析组别信息
234         */
235        @NotNull
236        private static Set<Object> parseGroups(@NotNull Annotation annotation, @NotNull Object value) {
237                String[] groups = getAnnotationValue(annotation, GROUP);
238                Set<Object> parsedGroups = new HashSet<>();
239                for (String group : groups) {
240                        parsedGroups.add(SpelParser.parse(group, value, Object.class));
241                }
242                return parsedGroups;
243        }
244
245        /**
246         * 自动填充校验结果的错误信息
247         */
248        private static void autoFillValidResult(@NotNull FieldValidResult result, @NotNull Annotation annotation, @NotNull Field verifiedField) {
249                if (!result.isSuccess()) {
250                        // 错误信息收集
251                        if (result.getMessage().isEmpty()) {
252                                String message = getAnnotationValue(annotation, MESSAGE);
253                                result.setMessage(message);
254                        }
255                        if (result.getFieldName().isEmpty()) {
256                                result.setFieldName(verifiedField.getName());
257                        }
258                }
259        }
260
261        /**
262         * 校验器实例管理器,避免重复创建校验器实例。
263         */
264        private static final ConcurrentHashMap<Class<? extends SpelConstraintValidator<?>>, SpelConstraintValidator<?>> VALIDATOR_INSTANCE_CACHE = new ConcurrentHashMap<>();
265
266        /**
267         * 获取校验器实例,当实例不存在时会创建一个新的实例。
268         */
269        @NotNull
270        private static <T extends SpelConstraintValidator<?>> SpelConstraintValidator<?> getValidatorInstance(@NotNull Class<T> validator) {
271                return VALIDATOR_INSTANCE_CACHE.computeIfAbsent(validator, key -> {
272                        try {
273                                return key.getDeclaredConstructor().newInstance();
274                        } catch (Exception e) {
275                                throw new SpelValidException("Create validator [" + validator.getName() + "] instance error: " + e.getMessage(), e);
276                        }
277                });
278        }
279
280        // todo 缓存
281        private static <T> T getAnnotationValue(@NotNull Annotation annotation, @NotNull String methodName) {
282                Method method = GetMethod.action(annotation.annotationType(), methodName).run();
283                try {
284                        //noinspection unchecked
285                        return (T) method.invoke(annotation);
286                } catch (Exception e) {
287                        throw new SpelValidException("Get method [" + annotation.annotationType().getName() + "." + methodName + "] error: " + e.getMessage(), e);
288                }
289        }
290
291}