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 }