View Javadoc
1   package com.github.mygreen.supercsv.io;
2   
3   import java.io.IOException;
4   import java.io.Reader;
5   import java.util.List;
6   
7   import org.supercsv.comment.CommentMatcher;
8   import org.supercsv.io.AbstractTokenizer;
9   import org.supercsv.prefs.CsvPreference;
10  import org.supercsv.util.CsvContext;
11  
12  import com.github.mygreen.supercsv.builder.BeanMapping;
13  import com.github.mygreen.supercsv.builder.ColumnMapping;
14  import com.github.mygreen.supercsv.builder.FixedSizeColumnProperty;
15  import com.github.mygreen.supercsv.exception.SuperCsvFixedSizeException;
16  import com.github.mygreen.supercsv.util.Utils;
17  
18  /**
19   * 固定長の行をカラムに分解するTokenizer。
20   *
21   * @since 2.5
22   * @author T.TSUCHIE
23   *
24   */
25  public class FixedSizeTokenizer extends AbstractTokenizer {
26  
27      /** 現在の行 */
28      private final StringBuilder currentRow = new StringBuilder();
29  
30      /** 空行を無視するかどうか。(CsvPreferenceで設定) */
31      private final boolean ignoreEmptyLines;
32  
33      /** コメント行判定。指定しない場合はnull。(CsvPreferenceで設定) */
34      private final CommentMatcher commentMatcher;
35  
36      /** カラム情報(固定長定義) */
37      private final List<ColumnMapping> columnMappings;
38  
39      public FixedSizeTokenizer(Reader reader, CsvPreference preferences, BeanMapping<?> beanMapping) {
40          super(reader, preferences);
41  
42          if (beanMapping.getColumns().isEmpty()) {
43              throw new IllegalArgumentException("columnMappings should not be empty.");
44          }
45  
46          this.ignoreEmptyLines = preferences.isIgnoreEmptyLines();
47          this.commentMatcher = preferences.getCommentMatcher();
48          this.columnMappings = beanMapping.getColumns();
49      }
50  
51      /**
52       * {@inheritDoc}
53       * 
54       * @throws NullPointerException {@literal if columns is null.}
55       */
56      @Override
57      public boolean readColumns(final List<String> columns) throws IOException {
58  
59          if( columns == null ) {
60              throw new NullPointerException("columns should not be null");
61          }
62  
63          columns.clear();
64  
65          // 空行、コメント行の読み飛ばし
66          String line;
67          do {
68              line = readLine();
69              if( line == null ) {
70                  return false; // EOF
71              }
72          }
73          while( ignoreEmptyLines && line.length() == 0 || (commentMatcher != null && commentMatcher.isComment(line)) );
74  
75          // update the untokenized CSV row
76          currentRow.append(line);
77  
78          final int[] codePointArray = Utils.toCodePointArray(line);
79  
80          int pos = 0;
81          for (ColumnMapping columnMapping : columnMappings) {
82              
83              if (pos >= codePointArray.length) {
84                  // 文字数が不足している場合
85                  break;
86              }
87              
88              StringBuilder text = new StringBuilder();
89              int lastPos = lastPosition(pos, codePointArray, text, columnMapping);
90  
91              /*
92               * 固定長の場合、エスケープ文字や途中改行などは対応しない。
93               * ・ライブラリ側でエスケープ文字を挿入すると文字数が変わり、固定長をオーバーしてしまうため。
94               * ・エスケープは、使用者側で行う。
95               */
96              columns.add(text.toString());
97  
98              pos = lastPos;
99          }
100         
101         // 文字列が余る場合は、最後のカラムとして読み込む。
102         if (pos < codePointArray.length) {
103             columns.add(new String(codePointArray, pos, codePointArray.length - pos));
104         }
105 
106         // process each character in the line
107         return true;
108     }
109 
110     /**
111      * カラム定義に従いカラムとして切り出し、最後の位置として引数 {@literal codePointArray} の配列のインデックスを取得する。
112      * 
113      * @param start カラムの開始位置。
114      * @param codePointArray 取得対象のCode Pointの配列。
115      * @param column カラムとして切り出した文字列を格納する。
116      * @param columnProperty 固定長カラムの定義情報。
117      * @return カラムの最後の位置としての引数 {@literal codePointArray} のインデックスを返す。
118      * @throws SuperCsvFixedSizeInsufficientException カラムのサイズが不足している場合。
119      */
120     private int lastPosition(final int start, int[] codePointArray, StringBuilder column, ColumnMapping columnMapping) {
121 
122         int pos = start;
123         int actualSize = 0;
124         final int arrayLength = codePointArray.length;
125         final FixedSizeColumnProperty fixedSizeColumnProperty = columnMapping.getFixedSizeProperty();
126         while(pos < arrayLength && actualSize < fixedSizeColumnProperty.getSize()) {
127             actualSize += fixedSizeColumnProperty.getPaddingProcessor().count(codePointArray[pos]);
128             pos++;
129         }
130         
131         if (actualSize < fixedSizeColumnProperty.getSize()) {
132             // カラムサイズに対して文字数が不足している場合は例外をスロー。
133             // rowNumberはここでは取得できないので仮値0を設定し、CsvReader側で値を補完する。
134             throw new SuperCsvFixedSizeException.Builder("csvError.fixedSizeInsufficient", new CsvContext(getLineNumber(), 0, columnMapping.getNumber()))
135                     .messageFormat("Insufficient column size. fixedColumnSize: %d, actualSize: %d",
136                             fixedSizeColumnProperty.getSize(), actualSize)
137                     .messageVariables("fixedColumnSize", fixedSizeColumnProperty.getSize())
138                     .messageVariables("actualSize", actualSize)
139                 .build();
140         }
141 
142         column.append(new String(codePointArray, start, pos - start));
143 
144         return pos;
145 
146     }
147 
148     @Override
149     public String getUntokenizedRow() {
150         return currentRow.toString();
151     }
152 }