4 * Copyright(c) 2009 olyutorskii
\r
5 * $Id: CsvExporter.java 953 2009-12-06 16:42:14Z olyutorskii $
\r
8 package jp.sourceforge.jindolf;
\r
10 import java.awt.Component;
\r
11 import java.awt.GridBagConstraints;
\r
12 import java.awt.GridBagLayout;
\r
13 import java.awt.Insets;
\r
14 import java.io.BufferedOutputStream;
\r
15 import java.io.File;
\r
16 import java.io.FileNotFoundException;
\r
17 import java.io.FileOutputStream;
\r
18 import java.io.IOException;
\r
19 import java.io.OutputStream;
\r
20 import java.io.OutputStreamWriter;
\r
21 import java.io.Writer;
\r
22 import java.nio.charset.Charset;
\r
23 import java.nio.charset.CharsetEncoder;
\r
24 import java.util.LinkedList;
\r
25 import java.util.List;
\r
26 import javax.swing.BorderFactory;
\r
27 import javax.swing.JComboBox;
\r
28 import javax.swing.JComponent;
\r
29 import javax.swing.JFileChooser;
\r
30 import javax.swing.JOptionPane;
\r
31 import javax.swing.JPanel;
\r
32 import javax.swing.border.Border;
\r
33 import javax.swing.filechooser.FileFilter;
\r
34 import jp.sourceforge.jindolf.corelib.TalkType;
\r
37 * 任意のPeriodの発言内容をCSVファイルへエクスポートする。
\r
38 * according to RFC4180 (text/csv)
\r
39 * @see <a href="http://www.ietf.org/rfc/rfc4180.txt">RFC4180</a>
\r
41 public final class CsvExporter{
\r
43 private static final String[] ENCNAMES = {
\r
61 private static final String JPCHECK =
\r
67 private static final String CSVEXT = ".csv";
\r
68 private static final char CR = '\r';
\r
69 private static final char LF = '\n';
\r
70 private static final String CRLF = CR +""+ LF;
\r
71 private static final int BUFSIZ = 1024;
\r
73 private static final List<Charset> CHARSET_LIST = buildCharsetList();
\r
74 private static final FileFilter CSV_FILTER = new CsvFileFilter();
\r
75 private static final JComboBox encodeBox = new JComboBox();
\r
76 private static final JFileChooser chooser = buildChooser();
\r
77 // TODO staticなGUIパーツってどうなんだ…
\r
80 * Charsetが日本語エンコーダを持っているか確認する。
\r
82 * @return 日本語エンコーダを持っていればtrue
\r
84 private static boolean hasJPencoder(Charset cs){
\r
85 if( ! cs.canEncode() ) return false;
\r
86 CharsetEncoder encoder = cs.newEncoder();
\r
88 if(encoder.canEncode(JPCHECK)) return true;
\r
89 }catch(Exception e){
\r
91 // 一部JRE1.5系の「x-euc-jp-linux」エンコーディング実装には
\r
92 // canEncode()が例外を投げるバグがあるので、その対処。
\r
98 * 日本語Charset一覧を生成する。
\r
99 * @return 日本語Charset一覧
\r
101 private static List<Charset> buildCharsetList(){
\r
102 List<Charset> csList = new LinkedList<Charset>();
\r
103 for(String name : ENCNAMES){
\r
104 if( ! Charset.isSupported(name) ) continue;
\r
105 Charset cs = Charset.forName(name);
\r
107 if(csList.contains(cs)) continue;
\r
109 if( ! hasJPencoder(cs) ) continue;
\r
114 Charset defcs = Charset.defaultCharset();
\r
115 if( defcs.name().equals("windows-31j")
\r
116 && Charset.isSupported("Shift_JIS") ){
\r
117 defcs = Charset.forName("Shift_JIS");
\r
120 if( hasJPencoder(defcs) || csList.size() <= 0 ){
\r
121 if(csList.contains(defcs)){
\r
122 csList.remove(defcs);
\r
124 csList.add(0, defcs);
\r
134 private static JFileChooser buildChooser(){
\r
135 JFileChooser result = new JFileChooser();
\r
137 result.setFileSelectionMode(JFileChooser.FILES_ONLY);
\r
138 result.setMultiSelectionEnabled(false);
\r
139 result.setFileHidingEnabled(true);
\r
141 result.setAcceptAllFileFilterUsed(true);
\r
143 result.setFileFilter(CSV_FILTER);
\r
145 JComponent accessory = buildAccessory();
\r
146 result.setAccessory(accessory);
\r
156 private static JComponent buildAccessory(){
\r
157 for(Charset cs : CHARSET_LIST){
\r
158 encodeBox.addItem(cs);
\r
161 Border border = BorderFactory.createTitledBorder("出力エンコード");
\r
162 encodeBox.setBorder(border);
\r
164 JPanel accessory = new JPanel();
\r
165 GridBagLayout layout = new GridBagLayout();
\r
166 GridBagConstraints constraints = new GridBagConstraints();
\r
167 accessory.setLayout(layout);
\r
169 constraints.insets = new Insets(3, 3, 3, 3);
\r
170 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
171 constraints.fill = GridBagConstraints.NONE;
\r
172 constraints.weightx = 0.0;
\r
173 constraints.weighty = 0.0;
\r
174 constraints.anchor = GridBagConstraints.NORTHWEST;
\r
176 accessory.add(encodeBox, constraints);
\r
178 constraints.fill = GridBagConstraints.BOTH;
\r
179 constraints.weightx = 1.0;
\r
180 constraints.weighty = 1.0;
\r
182 accessory.add(new JPanel(), constraints); // dummy
\r
188 * ファイルに書き込めない/作れないエラー用のダイアログを表示する。
\r
189 * @param file 書き込もうとしたファイル。
\r
191 private static void writeError(File file){
\r
192 Component parent = null;
\r
193 String title = "ファイル書き込みエラー";
\r
194 String message = "ファイル「" + file.toString() + "」\n"
\r
197 JOptionPane.showMessageDialog(parent, message, title,
\r
198 JOptionPane.ERROR_MESSAGE );
\r
204 * ファイル上書き確認ダイアログを表示する。
\r
205 * @param file 上書き対象ファイル
\r
206 * @return 上書きOKが指示されたらtrue
\r
208 private static boolean confirmOverwrite(File file){
\r
209 Component parent = null;
\r
210 String title = "上書き確認";
\r
211 String message = "既存のファイル「" + file.toString() + "」\n"
\r
212 +"を上書きしようとしています。続けますか?";
\r
214 int confirm = JOptionPane.showConfirmDialog(
\r
215 parent, message, title,
\r
216 JOptionPane.WARNING_MESSAGE,
\r
217 JOptionPane.OK_CANCEL_OPTION );
\r
219 if(confirm == JOptionPane.OK_OPTION) return true;
\r
226 * @param period エクスポート対象の日
\r
228 private static void setTitle(Period period){
\r
229 Village village = period.getVillage();
\r
230 String villageName = village.getVillageName();
\r
231 String title = villageName + "村 " + period.getCaption();
\r
232 title += "の発言をCSVファイルへエクスポートします";
\r
233 chooser.setDialogTitle(title);
\r
238 * エクスポート先ファイルの名前を生成する。
\r
239 * @param period エクスポート対象の日
\r
240 * @return エクスポートファイル名
\r
242 private static String createUniqueFileName(Period period){
\r
243 Village village = period.getVillage();
\r
244 String villageName = village.getVillageName();
\r
246 String base = "JIN_" + villageName;
\r
248 switch(period.getType()){
\r
250 base += "_Prologue";
\r
253 base += "_Epilogue";
\r
257 base += period.getDay();
\r
270 csvName += "("+ serial +")";
\r
275 File current = chooser.getCurrentDirectory();
\r
276 saveFile = new File(current, csvName);
\r
277 }while(saveFile.exists());
\r
285 * @param period ダンプ対象Period
\r
286 * @param topicFilter 発言フィルタ
\r
287 * @throws java.io.IOException 出力エラー
\r
289 private static void dumpPeriod(Appendable out,
\r
291 TopicFilter topicFilter)
\r
292 throws IOException{
\r
293 String day = String.valueOf(period.getDay());
\r
295 List<Topic> topicList = period.getTopicList();
\r
296 for(Topic topic : topicList){
\r
297 if( ! (topic instanceof Talk) ) continue;
\r
298 Talk talk = (Talk) topic;
\r
299 if(talk.getTalkCount() <= 0) continue;
\r
301 if(topicFilter.isFiltered(talk)) continue;
\r
303 Avatar avatar = talk.getAvatar();
\r
305 String name = avatar.getName();
\r
306 int hour = talk.getHour();
\r
307 int minute = talk.getMinute();
\r
308 TalkType type = talk.getTalkType();
\r
309 CharSequence dialog = talk.getDialog();
\r
311 out.append(name).append(',');
\r
313 out.append(day).append(',');
\r
315 out.append(Character.forDigit(hour / 10, 10));
\r
316 out.append(Character.forDigit(hour % 10, 10));
\r
318 out.append(Character.forDigit(minute / 10, 10));
\r
319 out.append(Character.forDigit(minute % 10, 10));
\r
323 case PUBLIC: out.append("say"); break;
\r
324 case PRIVATE: out.append("think"); break;
\r
325 case WOLFONLY: out.append("whisper"); break;
\r
326 case GRAVE: out.append("groan"); break;
\r
327 default: assert false; break;
\r
331 escapeCSV(out, dialog);
\r
339 * ダイアログ操作に従いPeriodをエクスポートする。
\r
340 * @param period エクスポート対象のPeriod
\r
341 * @param topicFilter 発言フィルタ
\r
342 * @return エクスポートしたファイル
\r
344 public static File exportPeriod(Period period, TopicFilter topicFilter){
\r
347 String uniqName = createUniqueFileName(period);
\r
348 File uniqFile = new File(uniqName);
\r
349 chooser.setSelectedFile(uniqFile);
\r
351 int result = chooser.showSaveDialog(null);
\r
353 if(result != JFileChooser.APPROVE_OPTION) return null;
\r
355 File selected = chooser.getSelectedFile();
\r
357 if( ! hasExtent(selected.getName()) ){
\r
358 FileFilter filter = chooser.getFileFilter();
\r
359 if(filter == CSV_FILTER){
\r
360 String path = selected.getPath();
\r
362 selected = new File(path);
\r
366 if(selected.exists()){
\r
367 if( ! selected.isFile() || ! selected.canWrite() ){
\r
368 writeError(selected);
\r
371 boolean confirmed = confirmOverwrite(selected);
\r
372 if( ! confirmed ) return null;
\r
376 created = selected.createNewFile();
\r
377 }catch(IOException e){
\r
378 writeError(selected);
\r
383 boolean confirmed = confirmOverwrite(selected);
\r
384 if( ! confirmed ) return null;
\r
390 os = new FileOutputStream(selected);
\r
391 }catch(FileNotFoundException e){
\r
392 writeError(selected);
\r
395 os = new BufferedOutputStream(os, BUFSIZ);
\r
397 Charset cs = (Charset)( encodeBox.getSelectedItem() );
\r
399 boolean hasIOError = false;
\r
400 Writer writer = new OutputStreamWriter(os, cs);
\r
402 dumpPeriod(writer, period, topicFilter);
\r
403 }catch(IOException e){
\r
408 }catch(IOException e){
\r
412 if(hasIOError) writeError(selected);
\r
418 * CSV用のエスケープシーケンス処理を行う。
\r
421 * @param seq エスケープシーケンス対象
\r
423 * @throws java.io.IOException 出力エラー
\r
425 public static Appendable escapeCSV(Appendable app, CharSequence seq)
\r
426 throws IOException{
\r
429 int length = seq.length();
\r
431 for(int pos = 0; pos < length; pos++){
\r
432 char ch = seq.charAt(pos);
\r
435 app.append("\"\"");
\r
452 * ファイル名が任意の拡張子を持つか判定する。
\r
454 * 拡張子の前は必ず一文字以上何かがなければならない。
\r
455 * @param filename ファイル名
\r
456 * @param extent '.'で始まる拡張子文字列
\r
457 * @return 指定された拡張子を持つならtrue
\r
459 public static boolean hasExtent(CharSequence filename,
\r
460 CharSequence extent ){
\r
461 int flength = filename.length();
\r
462 int elength = extent .length();
\r
463 if(elength < 2) return false;
\r
464 if(flength <= elength) return false;
\r
466 if(filename.charAt(0) == '.') return false;
\r
468 int offset = flength - elength;
\r
471 for(int pos = 0; pos < elength; pos++){
\r
472 char ech = Character.toLowerCase(extent .charAt(pos ));
\r
473 char fch = Character.toLowerCase(filename.charAt(pos + offset));
\r
474 if(fch != ech) return false;
\r
481 * パス名抜きのファイル名が拡張子を持つか判定する。
\r
482 * 先頭が.で始まるファイル名は拡張子を持たない。
\r
483 * 末尾が.で終わるファイル名は拡張子を持たない。
\r
484 * それ以外の.を含むファイル名は拡張子を持つとみなす。
\r
485 * @param filename パス名抜きのファイル名
\r
486 * @return 拡張子を持っていればtrue
\r
488 public static boolean hasExtent(CharSequence filename){
\r
489 int length = filename.length();
\r
490 if(length < 3) return false;
\r
492 if(filename.charAt(0) == '.') return false;
\r
493 int lastPos = length - 1;
\r
494 if(filename.charAt(lastPos) == '.') return false;
\r
496 for(int pos = 1; pos <= lastPos - 1; pos++){
\r
497 char ch = filename.charAt(pos);
\r
498 if(ch == '.') return true;
\r
507 private CsvExporter(){
\r
509 throw new AssertionError();
\r
514 * 名前が「*.csv」の通常ファイルとディレクトリのみ表示させる。
\r
515 * ※ 表示の可否を問うものであって、選択の可否を問うものではない。
\r
517 private static class CsvFileFilter extends FileFilter{
\r
522 public CsvFileFilter(){
\r
529 * @param file {@inheritDoc}
\r
530 * @return {@inheritDoc}
\r
532 public boolean accept(File file){
\r
533 if(file.isDirectory()) return true;
\r
534 if( ! file.isFile() ) return false;
\r
536 if( ! hasExtent(file.getName(), CSVEXT) ) return false;
\r
543 * @return {@inheritDoc}
\r
545 public String getDescription(){
\r
546 return "CSVファイル (*.csv)";
\r
550 // TODO SecurityExceptionの捕捉
\r