View Javadoc
1   package com.github.mygreen.supercsv.io;
2   
3   import java.io.BufferedReader;
4   import java.io.IOException;
5   import java.io.Reader;
6   import java.util.ArrayList;
7   import java.util.List;
8   import java.util.Objects;
9   import java.util.Optional;
10  import java.util.stream.Collectors;
11  
12  import org.supercsv.exception.SuperCsvException;
13  import org.supercsv.io.ITokenizer;
14  import org.supercsv.prefs.CsvPreference;
15  
16  import com.github.mygreen.supercsv.annotation.CsvPartial;
17  import com.github.mygreen.supercsv.builder.BeanMapping;
18  import com.github.mygreen.supercsv.builder.BeanMappingFactoryHelper;
19  import com.github.mygreen.supercsv.builder.ColumnMapping;
20  import com.github.mygreen.supercsv.builder.HeaderMapper;
21  import com.github.mygreen.supercsv.builder.LazyBeanMappingFactory;
22  import com.github.mygreen.supercsv.exception.SuperCsvBindingException;
23  import com.github.mygreen.supercsv.exception.SuperCsvNoMatchColumnSizeException;
24  import com.github.mygreen.supercsv.exception.SuperCsvNoMatchHeaderException;
25  import com.github.mygreen.supercsv.localization.MessageBuilder;
26  
27  /**
28   * カラムの定義が曖昧なアノテーションを元にCSVファイルを読み込むためのクラス。
29   * <p>カラム番号が指定されていないBean定義を元にマッピングします。
30   *   <br>カラム番号の決定は、ヘッダー行を取得して、その情報を元に読み込み時に決定します。
31   * </p>
32   * 
33   * @param <T> マッピング対象のBeanのクラスタイプ
34   * 
35   * @version 2.2
36   * @since 2.1
37   * @author T.TSUCHIE
38   *
39   */
40  public class LazyCsvAnnotationBeanReader<T> extends AbstractCsvAnnotationBeanReader<T> {
41      
42      /**
43       * Beanのマッピング情報。
44       * ・初期化は済んでいない場合があるため、キャッシュとは別に管理する。
45       */
46      private final BeanMapping<T> beanMapping;
47      
48      /**
49       * ヘッダー情報を元に初期化済みかどうか
50       */
51      private boolean initialized = false;
52      
53      /**
54       * Beanのクラスタイプを指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
55       * <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
56       * 
57       * @param beanType Beanのクラスタイプ。
58       * @param reader the Reader。
59       * @param preference the CSV preferences.
60       * @param groups グループ情報。適用するアノテーションを切り替える際に指定します。
61       * @throws NullPointerException {@literal if beanType or reader or preferences are null.}
62       */
63      public LazyCsvAnnotationBeanReader(final Class<T> beanType, final Reader reader, final CsvPreference preference,
64              final Class<?>... groups) {
65          
66          super(reader, preference);
67          
68          Objects.requireNonNull(beanType, "beanType should not be null.");
69          
70          LazyBeanMappingFactoryanMappingFactory.html#LazyBeanMappingFactory">LazyBeanMappingFactory factory = new LazyBeanMappingFactory();
71          this.beanMapping = factory.create(beanType, groups);
72          this.validators.addAll(beanMapping.getValidators());
73      }
74      
75      /**
76       * Beanのマッピング情報を指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
77       * <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
78       * <p>Beanのマッピング情報を独自にカスタマイズして、{@link LazyBeanMappingFactory}から作成する場合に利用します。</p>
79       * 
80       * @param beanMapping Beanのマッピング情報。
81       * @param reader the Reader。
82       * @param preference the CSV preferences.
83       * @throws NullPointerException {@literal if beanMapping or reader or preferences are null.}
84       */
85      public LazyCsvAnnotationBeanReader(final BeanMapping<T> beanMapping, final Reader reader, final CsvPreference preference,
86              final Class<?>... groups) {
87          
88          super(reader, preference);
89          
90          Objects.requireNonNull(beanMapping, "beanMapping should not be null.");
91          
92          this.beanMapping = beanMapping;
93          this.validators.addAll(beanMapping.getValidators());
94      }
95      
96      /**
97       * Beanのクラスタイプを指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
98       * <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
99       * 
100      * @param beanType Beanのクラスタイプ。
101      * @param tokenizer the tokenizer.
102      * @param preference the CSV preferences.
103      * @param groups グループ情報。適用するアノテーションを切り替える際に指定します。
104      * @throws NullPointerException {@literal if beanType or tokenizer or preferences are null.}
105      */
106     public LazyCsvAnnotationBeanReader(final Class<T> beanType, final ITokenizer tokenizer, final CsvPreference preference,
107             final Class<?>... groups) {
108         
109         super(tokenizer, preference);
110         
111         Objects.requireNonNull(beanType, "beanType should not be null.");
112         
113         LazyBeanMappingFactoryanMappingFactory.html#LazyBeanMappingFactory">LazyBeanMappingFactory factory = new LazyBeanMappingFactory();
114         this.beanMapping = factory.create(beanType, groups);
115         this.validators.addAll(beanMapping.getValidators());
116     }
117     
118     /**
119      * Beanのマッピング情報を指定して、{@link LazyCsvAnnotationBeanReader}を作成するコンストラクタ。
120      * <p>{@link BufferedReader}にラップして実行されるため、ラップする必要はありません。</p>
121      * <p>Beanのマッピング情報を独自にカスタマイズして、{@link LazyBeanMappingFactory}から作成する場合に利用します。</p>
122      * 
123      * @param beanMapping Beanのマッピング情報。
124      * @param tokenizer the tokenizer.
125      * @param preference the CSV preferences.
126      * @param groups グループ情報。適用するアノテーションを切り替える際に指定します。
127      * @throws NullPointerException {@literal if beanMapping or tokenizer or preferences are null.}
128      */
129     public LazyCsvAnnotationBeanReader(final BeanMapping<T> beanMapping, final ITokenizer tokenizer, final CsvPreference preference,
130             final Class<?>... groups) {
131         
132         super(tokenizer, preference);
133         
134         Objects.requireNonNull(beanMapping, "beanMapping should not be null.");
135         
136         this.beanMapping = beanMapping;
137         this.validators.addAll(beanMapping.getValidators());
138     }
139     
140     /**
141      * 1行目のレコードをヘッダー情報として読み込んで、カラム情報を初期化を行います。
142      * 
143      * @return 読み込んだヘッダー情報
144      * @throws SuperCsvNoMatchColumnSizeException ヘッダーのサイズ(カラム数)がBean定義と一致しない場合。
145      * @throws SuperCsvNoMatchHeaderException ヘッダーの値がBean定義と一致しない場合。
146      * @throws SuperCsvException 引数firstLineCheck=trueのとき、このメソッドが1行目以外の読み込み時に呼ばれた場合。
147      * @throws IOException ファイルの読み込みに失敗した場合。
148      */
149     public String[] init() throws IOException {
150         // ヘッダーを元に、カラム情報の番号を補完する
151         final String[] headers = getHeader(true);
152         init(headers);
153         
154         return headers;
155     }
156     
157     /**
158      * ヘッダー情報を指定して、カラム情報の初期化を行います。
159      * <p>ヘッダーの位置を元にカラムの番号を決定します。</p>
160      * 
161      * @param headers CSVのヘッダー情報。実際のCSVファイルの内容と一致する必要があります。
162      * @throws SuperCsvNoMatchColumnSizeException ヘッダーのサイズ(カラム数)がBean定義と一致しない場合。
163      * @throws SuperCsvNoMatchHeaderException ヘッダーの値がBean定義と一致しない場合。
164      * @throws SuperCsvException 引数firstLineCheck=trueのとき、このメソッドが1行目以外の読み込み時に呼ばれた場合。
165      */
166     public void init(final String... headers) {
167         
168         setupMappingColumns(headers);
169         this.beanMappingCache = BeanMappingCache.create(beanMapping);
170         
171         if(beanMappingCache.getOriginal().isValidateHeader()) {
172             try {
173                 validateHeader(headers, beanMapping.getHeader());
174                 
175             } catch(SuperCsvNoMatchColumnSizeException | SuperCsvNoMatchHeaderException e) {
176                 // convert exception and format to message.
177                 errorMessages.addAll(exceptionConverter.convertAndFormat(e, beanMappingCache.getOriginal()));
178                 throw e;
179             }
180         }
181         
182         // 初期化完了
183         this.initialized = true;
184     }
185     
186     
187     /**
188      * 初期化が完了していないときに呼ばれたときにスローする例外のインスタンスを作成します。
189      */
190     private IllegalStateException newNotInitialzedException() {
191         return new IllegalStateException(MessageBuilder.create("noinit.onLazyRead").format());
192     }
193     
194     /**
195      * レコードを全て読み込みます。
196      * <p>ヘッダー行も自動的に処理されます。</p>
197      * <p>レコード処理中に例外が発生した場合、その時点で処理を終了します。</p>
198      * 
199      * @return 読み込んだレコード情報。
200      * 
201      * @throws IOException レコードの読み込みに失敗した場合。
202      * @throws SuperCsvNoMatchColumnSizeException レコードのカラムサイズに問題がある場合
203      * @throws SuperCsvBindingException セルの値に問題がある場合
204      * @throws SuperCsvException 設定など、その他に問題がある場合
205      * @throws IllegalStateException ヘッダー行を持たないときに、{@link #init(String...)}で初期化が済んでいない場合。
206      */
207     public List<T> readAll() throws IOException {
208         return readAll(false);
209     }
210     
211     /**
212      * レコードを全て読み込みます。
213      * <p>ヘッダー行も自動的に処理されます。</p>
214      * 
215      * @param continueOnError レコードの処理中に、
216      *        例外{@link SuperCsvNoMatchColumnSizeException}、{@link SuperCsvNoMatchColumnSizeException}、{@link SuperCsvBindingException}
217      *        が発生しても続行するかどう指定します。
218      *        trueの場合、例外が発生しても、次の処理を行います。
219      * @return 読み込んだレコード情報。
220      * 
221      * @throws IOException レコードの読み込みに失敗した場合。
222      * @throws SuperCsvNoMatchColumnSizeException レコードのカラムサイズに問題がある場合
223      * @throws SuperCsvBindingException セルの値に問題がある場合
224      * @throws SuperCsvException 設定など、その他に問題がある場合
225      * @throws IllegalStateException ヘッダー行を持たないときに、{@link #init(String...)}で初期化が済んでいない場合。
226      * 
227      */
228     public List<T> readAll(final boolean continueOnError) throws IOException {
229         
230         if(!initialized) {
231             if(beanMapping.isHeader()) {
232                 // ヘッダーがファイルに存在する場合、1行目を読み込んで初期化を行う。
233                 try {
234                     init();
235                 } catch(SuperCsvNoMatchColumnSizeException | SuperCsvNoMatchHeaderException e) {
236                     if(!continueOnError) {
237                         throw e;
238                     }
239                 }
240                 
241             } else {
242                 // ヘッダーがファイルに存在しない場合、独自にinit(header1, header2)メソッドを呼んで初期化する必要がある。
243                 throw newNotInitialzedException();
244             }
245         }
246         
247         final List<T> list = new ArrayList<>();
248         
249         while(true) {
250             try {
251                 final T record = read();
252                 if(record == null) {
253                     break;
254                 }
255                 list.add(record);
256                 
257             } catch(SuperCsvNoMatchColumnSizeException | SuperCsvBindingException e) {
258                 if(!continueOnError) {
259                     throw e;
260                 }
261             }
262         }
263         
264         return list;
265         
266         
267     }
268     
269     /**
270      * {@inheritDoc}
271      * @throws IllegalStateException ヘッダーが読み込まれておらず、マッピング情報の初期か完了していない場合。
272      */
273     @Override
274     public T read() throws IOException {
275         
276         // ヘッダーが読み込まれておらず、初期化が終わっていない場合
277         if(!initialized) {
278             throw newNotInitialzedException();
279         }
280         
281         return super.read();
282         
283         
284     }
285     
286     /**
287      * 読み込んだヘッダーを元に、マッピング情報を補完する。
288      * <p>カラムの位置である番号を確定する。</p>
289      * <p>存在しないカラムがある場合は、部分的な読み込みとして、ダミーのカラム情報を作成する。</p>
290      * @param headers
291      * @param columns
292      * @param SuperCsvInvalidAnnotationException
293      */
294     private void setupMappingColumns(final String[] headers) {
295         
296         final List<ColumnMapping> columnMappingList = beanMapping.getColumns();
297         final HeaderMapper headerMapper = beanMapping.getHeaderMapper();
298         
299         // 一致するラベルがあれば、カラムの番号を補完する
300         final int headerSize = headers.length;
301         for(int i=0; i < headerSize;i ++) {
302             
303             final String header = headers[i];
304             
305             /*
306              * 番号が決まっておらず、ラベルが一致するカラム情報を抽出する。
307              * ※既に番号が決まっているが、ラベルが一致しないのものは、後からチェックする。
308              */
309             List<ColumnMapping> undeterminedColumnList = columnMappingList.stream()
310                 .filter(col -> col.getNumber() <= 0)
311                 .filter(col -> headerMapper.toMap(col, beanMapping.getConfiguration(), beanMapping.getGroups()).equals(header))
312                 .collect(Collectors.toList());
313             
314             final int columnNumber = i+1;
315             undeterminedColumnList.forEach(col -> col.setNumber(columnNumber));
316             
317         }
318         
319         // カラムの番号順に並び変える
320         columnMappingList.sort(null);
321         
322         // 決定していないカラム番号のチェック
323         BeanMappingFactoryHelper.validateNonDeterminedColumnNumber(beanMapping.getType(), columnMappingList, headers);
324         
325         // 重複しているカラム番号のチェック
326         BeanMappingFactoryHelper.validateDuplicatedColumnNumber(beanMapping.getType(), columnMappingList);
327         
328         // 不足しているカラム番号の補完
329         final Optional<CsvPartial> partialAnno = Optional.ofNullable(beanMapping.getType().getAnnotation(CsvPartial.class));
330         BeanMappingFactoryHelper.supplyLackedNumberMappingColumn(beanMapping.getType(), columnMappingList, partialAnno, headers, beanMapping.getConfiguration());
331         
332         beanMapping.setColumns(columnMappingList);
333         
334     }
335     
336     /**
337      * {@inheritDoc}
338      * @throws IllegalStateException {@link #init()} メソッドによる初期化が完了していない場合
339      */
340     @Override
341     public String[] getDefinedHeader() {
342         if(!initialized) {
343             throw newNotInitialzedException();
344         }
345         
346         return super.getDefinedHeader();
347     }
348     
349     /**
350      * {@inheritDoc}
351      * @throws IllegalStateException {@link #init()} メソッドによる初期化が完了していない場合
352      */
353     @Override
354     public BeanMapping<T> getBeanMapping() {
355         if(!initialized) {
356             throw newNotInitialzedException();
357         }
358         
359         return super.getBeanMapping();
360     }
361     
362     
363 }