View Javadoc
1   /*
2    * Copyright 2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.orangesignal.csv;
18  
19  import java.io.BufferedWriter;
20  import java.io.Closeable;
21  import java.io.Flushable;
22  import java.io.IOException;
23  import java.io.OutputStreamWriter;
24  import java.io.Writer;
25  import java.nio.charset.Charset;
26  import java.util.ArrayList;
27  import java.util.List;
28  
29  /**
30   * 区切り文字形式出力ストリームを提供します。
31   *
32   * @author Koji Sugisawa
33   */
34  public class CsvWriter implements Closeable, Flushable {
35  
36  	/**
37  	 * 文字出力ストリームを保持します。
38  	 */
39  	private Writer out;
40  
41  	/**
42  	 * 区切り文字形式情報を保持します。
43  	 */
44  	private CsvConfig cfg;
45  
46  	/**
47  	 * BOM (Byte Order Mark) を出力する必要があるかどうかを保持します。
48  	 */
49  	private boolean utf8bom;
50  
51  	/**
52  	 * 項目数チェックの為に直前の行の項目数を保持します。
53  	 */
54  	private int countNumberOfColumns = -1;
55  
56  	private static final int DEFAULT_CHAR_BUFFER_SIZE = 8192;
57  
58  	// ------------------------------------------------------------------------
59  	// コンストラクタ
60  
61  	/**
62  	 * 指定されたバッファーサイズと指定された区切り文字形式情報を使用して、このクラスを構築するコンストラクタです。
63  	 *
64  	 * @param out 文字出力ストリーム
65  	 * @param sz 出力バッファのサイズ
66  	 * @param cfg 区切り文字形式情報
67  	 * @throws IllegalArgumentException {@code sz} が {@code 0} 以下の場合。または、{@code cfg} が {@code null} の場合
68  	 * または、{@code cfg} の区切り文字および囲み文字、エスケープ文字の組合せが不正な場合
69  	 */
70  	public CsvWriter(final Writer out, final int sz, final CsvConfig cfg) {
71  		if (cfg == null) {
72  			throw new IllegalArgumentException("CsvConfig must not be null");
73  		}
74  		cfg.validate();
75  		this.out = new BufferedWriter(out, sz);
76  		this.cfg = cfg;
77  
78  		if (cfg.isUtf8bomPolicy()) {
79  			final String s;
80  			if (out instanceof OutputStreamWriter) {
81  				s = ((OutputStreamWriter) out).getEncoding();
82  			} else {
83  				s = Charset.defaultCharset().name();
84  			}
85  			this.utf8bom = s.toLowerCase().matches("^utf\\-{0,1}8$");
86  		}
87  	}
88  
89  	/**
90  	 * デフォルトのバッファーサイズと指定された区切り文字形式情報を使用して、このクラスを構築するコンストラクタです。
91  	 *
92  	 * @param out 文字出力ストリーム
93  	 * @param cfg 区切り文字形式情報
94  	 * @throws IllegalArgumentException {@code cfg} が {@code null} の場合
95  	 * または、{@code cfg} の区切り文字および囲み文字、エスケープ文字の組合せが不正な場合
96  	 */
97  	public CsvWriter(final Writer out, final CsvConfig cfg) {
98  		this(out, DEFAULT_CHAR_BUFFER_SIZE, cfg);
99  	}
100 
101 	/**
102 	 * 指定されたバッファーサイズとデフォルトの区切り文字形式情報を使用して、このクラスを構築するコンストラクタです。
103 	 *
104 	 * @param out 文字出力ストリーム
105 	 * @param sz 出力バッファのサイズ
106 	 * @throws IllegalArgumentException {@code sz} が {@code 0} 以下の場合
107 	 */
108 	public CsvWriter(final Writer out, final int sz) {
109 		this(out, sz, new CsvConfig());
110 	}
111 
112 	/**
113 	 * デフォルトのバッファーサイズとデフォルトの区切り文字形式情報を使用して、このクラスを構築するコンストラクタです。
114 	 *
115 	 * @param out 文字出力ストリーム
116 	 */
117 	public CsvWriter(final Writer out) {
118 		this(out, DEFAULT_CHAR_BUFFER_SIZE, new CsvConfig());
119 	}
120 
121 	// ------------------------------------------------------------------------
122 
123 	/**
124 	 * Checks to make sure that the stream has not been closed
125 	 */
126 	private void ensureOpen() throws IOException {
127 		if (out == null) {
128 			throw new IOException("Stream closed");
129 		}
130 	}
131 
132 	private static final int BOM = 0xFEFF;
133 
134 	/**
135 	 * 指定された CSV トークンの値リストを書き込みます。
136 	 *
137 	 * @param values 書き込む CSV トークンの値リスト
138 	 * @throws CsvValueException 可変項目数が禁止されている場合に項目数が一致しない場合
139 	 * @throws IOException 入出力エラーが発生した場合
140 	 */
141 	public void writeValues(final List<String> values) throws IOException {
142 		synchronized (this) {
143 			ensureOpen();
144 
145 			if (utf8bom) {
146 				out.write(BOM);
147 				utf8bom = false;
148 			}
149 
150 			final StringBuilder buf = new StringBuilder();
151 			if (values != null) {
152 				final int max = values.size();
153 				for (int i = 0; i < max; i++) {
154 					if (i > 0) {
155 						buf.append(cfg.getSeparator());
156 					}
157 	
158 					String value = values.get(i);
159 					boolean enclose = false;	// 項目を囲み文字で囲むかどうか
160 					if (value == null) {
161 						// 項目値が null の場合に NULL 文字列が有効であれば NULL 文字列へ置換えます。
162 						if (cfg.getNullString() == null) { 
163 							continue;
164 						}
165 						value = cfg.getNullString();
166 					} else if (!cfg.isQuoteDisabled()) {
167 						// 囲み文字が有効な場合は、囲み文字で囲むべきかどうか判断します。
168 						switch (cfg.getQuotePolicy()) {
169 							case ALL:
170 								enclose = true;
171 								break;
172 
173 							case MINIMAL:
174 							default:
175 								// 項目値に区切り文字、囲み文字、改行文字のいずれかを含む場合は囲み文字で囲むべきと判断します。
176 								enclose = value.indexOf(cfg.getSeparator()) != -1
177 										|| value.indexOf(cfg.getQuote()) != -1
178 										|| value.indexOf('\r') != -1 || value.indexOf('\n') != -1;
179 								break;
180 						}
181 					} else {
182 						// 囲み文字が無効な場合に、項目値に区切り文字がある場合、エスケープします。
183 						final String s = escapeSeparator(value);
184 						if (!value.equals(s) && cfg.isEscapeDisabled()) {
185 							throw new IOException();
186 						}
187 						value = s;
188 					}
189 	
190 					if (enclose) {
191 						buf.append(cfg.getQuote());
192 						final String s = escapeQuote(value);
193 						if (!value.equals(s) && cfg.isEscapeDisabled()) {
194 							throw new IOException();
195 						}
196 						buf.append(s);
197 						buf.append(cfg.getQuote());
198 					} else {
199 						buf.append(value);
200 					}
201 				}
202 			}
203 			if (values != null || !cfg.isIgnoreEmptyLines()) {
204 				buf.append(cfg.getLineSeparator());
205 				out.write(buf.toString());
206 			}
207 			if (!cfg.isVariableColumns() && values != null) {
208 				if (countNumberOfColumns >= 0 && countNumberOfColumns != values.size()) {
209 					throw new CsvValueException(String.format("Invalid column count."), values);
210 				}
211 				countNumberOfColumns = values.size();
212 			}
213 		}
214 	}
215 
216 	/**
217 	 * 指定された CSV トークンのリストを書き込みます。
218 	 * 
219 	 * @param tokens 書き込む CSV トークンのリスト
220 	 * @throws CsvValueException 可変項目数が禁止されている場合に項目数が一致しない場合
221 	 * @throws IOException 入出力エラーが発生した場合
222 	 */
223 	public void writeTokens(final List<CsvToken> tokens) throws IOException {
224 		if (tokens != null) {
225 			final List<String> values = new ArrayList<String>(tokens.size());
226 			for (final CsvToken token : tokens) {
227 				if (token == null) {
228 					values.add(null);
229 				} else {
230 					values.add(token.getValue());
231 				}
232 			}
233 			writeValues(values);
234 		} else {
235 			writeValues(null);
236 		}
237 	}
238 
239 	/**
240 	 * 指定された文字列中の区切り文字をエスケープ化して返します。
241 	 * 
242 	 * @param value 文字列
243 	 * @return エスケープされた文字列
244 	 */
245 	private String escapeSeparator(final String value) {
246 		return value.replace(
247 				new StringBuilder(1).append(cfg.getSeparator()),
248 				new StringBuilder(2).append(cfg.getEscape()).append(cfg.getSeparator())
249 			);
250 	}
251 
252 	/**
253 	 * 指定された文字列中の囲み文字をエスケープ化して返します。
254 	 *
255 	 * @param value 文字列
256 	 * @return エスケープされた文字列
257 	 */
258 	private String escapeQuote(final String value) {
259 		return value.replace(
260 				new StringBuilder(1).append(cfg.getQuote()),
261 				new StringBuilder(2).append(cfg.getEscape()).append(cfg.getQuote())
262 			);
263 	}
264 
265 	@Override
266 	public void flush() throws IOException {
267 		synchronized (this) {
268 			ensureOpen();
269 			out.flush();
270 		}
271 	}
272 
273 	@Override
274 	public void close() throws IOException {
275 		if (out != null) {
276 			out.close();
277 			out = null;
278 			cfg = null;
279 		}
280 	}
281 
282 }