View Javadoc
1   package com.github.mygreen.supercsv.validation.beanvalidation;
2   
3   import java.lang.reflect.Method;
4   import java.util.Collections;
5   import java.util.HashMap;
6   import java.util.HashSet;
7   import java.util.Map;
8   import java.util.Objects;
9   import java.util.Set;
10  
11  import org.slf4j.Logger;
12  import org.slf4j.LoggerFactory;
13  
14  import com.github.mygreen.supercsv.builder.ColumnMapping;
15  import com.github.mygreen.supercsv.localization.MessageInterpolator;
16  import com.github.mygreen.supercsv.localization.ResourceBundleMessageResolver;
17  import com.github.mygreen.supercsv.validation.CsvBindingErrors;
18  import com.github.mygreen.supercsv.validation.CsvFieldError;
19  import com.github.mygreen.supercsv.validation.CsvValidator;
20  import com.github.mygreen.supercsv.validation.ValidationContext;
21  
22  import jakarta.validation.ConstraintViolation;
23  import jakarta.validation.Validation;
24  import jakarta.validation.Validator;
25  import jakarta.validation.ValidatorFactory;
26  import jakarta.validation.metadata.ConstraintDescriptor;
27  
28  /**
29   * Jakarata Bean Validaion 3.0/3.1 を利用したValidatorにブリッジする{@link CsvValidator}。
30   * 
31   * @since 2.4
32   * @author T.TSUCHIE
33   *
34   */
35  public class JakartaCsvBeanValidator implements CsvValidator<Object> {
36      
37      private static final Logger logger = LoggerFactory.getLogger(JakartaCsvBeanValidator.class);
38      
39      /**
40       * BeanValidationのアノテーションの属性で、メッセージ中の変数から除外するもの。
41       * <p>メッセージの再構築を行う際に必要
42       */
43      private static final Set<String> EXCLUDE_MESSAGE_ANNOTATION_ATTRIBUTES;
44      static {
45          Set<String> set = new HashSet<String>(3);
46          set.add("message");
47          set.add("groups");
48          set.add("payload");
49          
50          EXCLUDE_MESSAGE_ANNOTATION_ATTRIBUTES = Collections.unmodifiableSet(set);
51      }
52      
53      private final Validator targetValidator;
54      
55      public JakartaCsvBeanValidator(final Validator targetValidator) {
56          Objects.requireNonNull(targetValidator);
57          this.targetValidator = targetValidator;
58      }
59      
60      public JakartaCsvBeanValidator() {
61          this.targetValidator = createDefaultValidator();
62      }
63      
64      /**
65       * Bean Validatonのデフォルトのインスタンスを取得する。
66       * @return Validatorを取得します。
67       */
68      private Validator createDefaultValidator() {
69          final ValidatorFactory validatorFactory = Validation.byDefaultProvider().configure()
70                  .messageInterpolator(new JakartaMessageInterpolatorAdapter(new ResourceBundleMessageResolver(), new MessageInterpolator()))
71                  .buildValidatorFactory();
72          
73          final Validator validator = validatorFactory.usingContext()
74                  .getValidator();
75          
76          return validator;
77      }
78      
79      /**
80       * BeanValidationのValidatorを取得する。
81       * @return Validatorを取得します。
82       */
83      public Validator getTargetValidator() {
84          return targetValidator;
85      }
86      
87      @Override
88      public void validate(final Object record, final CsvBindingErrors bindingErrors, 
89              final ValidationContext<Object> validationContext) {
90          validate(record, bindingErrors, validationContext, validationContext.getBeanMapping().getGroups());
91          
92      }
93      
94      /**
95       * グループを指定して検証を実行する。
96       * @param record 検証対象のオブジェクト。
97       * @param bindingErrors エラーオブジェクト
98       * @param validationContext 入力値検証のためのコンテキスト情報
99       * @param groups BeanValiationのグループのクラス
100      */
101     public void validate(final Object record, final CsvBindingErrors bindingErrors, 
102             final ValidationContext<Object> validationContext, final Class<?>... groups) {
103         Objects.requireNonNull(record);
104         Objects.requireNonNull(bindingErrors);
105         Objects.requireNonNull(validationContext);
106         
107         processConstraintViolation(getTargetValidator().validate(record, groups), bindingErrors, validationContext);
108     }
109     
110     /**
111      * BeanValidationの検証結果をSheet用のエラーに変換する
112      * @param violations BeanValidationの検証結果
113      * @param bindingErrors エラー情報
114      * @param validationContext 入力値検証のためのコンテキスト情報
115      */
116     private void processConstraintViolation(final Set<ConstraintViolation<Object>> violations,
117             final CsvBindingErrors bindingErrors, final ValidationContext<Object> validationContext) {
118         
119         for(ConstraintViolation<Object> violation : violations) {
120             
121             final String field = violation.getPropertyPath().toString();
122             final ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
123             
124             final String[] errorCodes = determineErrorCode(cd);
125             
126             final Map<String, Object> errorVars = createVariableForConstraint(cd);
127             
128             if(isCsvField(field, validationContext)) {
129                 // フィールドエラーの場合
130                 
131                 final CsvFieldError fieldError = bindingErrors.getFirstFieldError(field);
132                 if(fieldError != null && fieldError.isProcessingFailure()) {
133                     // CellProcessorで発生したエラーが既ににある場合は、処理をスキップする。
134                     continue;
135                 }
136                 
137                 final ColumnMapping columnMapping = validationContext.getBeanMapping().getColumnMapping(field).get();
138                 
139                 errorVars.put("lineNumber", validationContext.getCsvContext().getLineNumber());
140                 errorVars.put("rowNumber", validationContext.getCsvContext().getRowNumber());
141                 errorVars.put("columnNumber", columnMapping.getNumber());
142                 errorVars.put("label", columnMapping.getLabel());
143                 errorVars.computeIfAbsent("printer", key -> columnMapping.getFormatter());
144                 
145                 // 実際の値を取得する
146                 final Object fieldValue = violation.getInvalidValue();
147                 errorVars.computeIfAbsent("validatedValue", key -> fieldValue);
148                 
149                 bindingErrors.rejectValue(field, columnMapping.getField().getType(), 
150                         errorCodes, errorVars, violation.getMessageTemplate());
151                 
152             } else {
153                 // オブジェクトエラーの場合
154                 bindingErrors.reject(errorCodes, errorVars, violation.getMessageTemplate());
155                 
156             }
157             
158         }
159         
160         
161     }
162     
163     /**
164      * エラーコードを決定する。
165      * <p>※ユーザ指定メッセージの場合はエラーコードは空。</p>
166      * 
167      * @since 2.4
168      * @param descriptor フィールド情報
169      * @return エラーコード
170      */
171     protected String[] determineErrorCode(final ConstraintDescriptor<?> descriptor) {
172         
173         // バリデーション用アノテーションから属性「message」のでデフォルト値を取得し、変更されているかどう比較する。
174         String defaultMessage = null;
175         try {
176             Method messageMethod = descriptor.getAnnotation().annotationType().getMethod("message");
177             messageMethod.setAccessible(true);
178             defaultMessage = Objects.toString(messageMethod.getDefaultValue(), null);
179         } catch (NoSuchMethodException | SecurityException e) {
180             logger.warn("Fail getting annotation's attribute 'message' for " + descriptor.getAnnotation().annotationType().getSimpleName() , e);
181         }
182         
183         if(!descriptor.getMessageTemplate().equals(defaultMessage)) {
184             /*
185              * アノテーション属性「message」の値がデフォルト値から変更されている場合は、
186              * ユーザー指定メッセージとして判断し、エラーコードは空にしてユーザー指定メッセージを優先させる。
187              */
188             return new String[]{};
189             
190         } else {
191             // アノテーションのクラス名をもとに生成する。
192             return new String[]{
193                     descriptor.getAnnotation().annotationType().getSimpleName(),
194                     descriptor.getAnnotation().annotationType().getCanonicalName(),
195                     descriptor.getAnnotation().annotationType().getCanonicalName() + ".message"
196             };
197         }
198     }
199     
200     /**
201      * CSVのカラムのフィールドか判定する。
202      * @param field フィールド名
203      * @param validationContext CSVの検証情報
204      * @return trueのとき、CSVのカラムフィールド。
205      */
206     private boolean isCsvField(final String field, final ValidationContext<Object> validationContext) {
207         return validationContext.getBeanMapping().getColumnMapping(field).isPresent();
208     }
209     
210     /**
211      * BeanValidationのアノテーションの値を元に、メッセージ変数を作成する。
212      * @param descriptor
213      * @return メッセージ変数
214      */
215     private Map<String, Object> createVariableForConstraint(final ConstraintDescriptor<?> descriptor) {
216         
217         final Map<String, Object> vars = new HashMap<>();
218         
219         for(Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) {
220             final String attrName = entry.getKey();
221             final Object attrValue = entry.getValue();
222             
223             // メッセージ変数で必要ないものを除外する
224             if(EXCLUDE_MESSAGE_ANNOTATION_ATTRIBUTES.contains(attrName)) {
225                 continue;
226             }
227             
228             vars.put(attrName, attrValue);
229         }
230         
231         return vars;
232         
233     }
234     
235 }