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