-/*\r
- * CSV file exporter\r
- *\r
- * Copyright(c) 2009 olyutorskii\r
- * $Id: CsvExporter.java 953 2009-12-06 16:42:14Z olyutorskii $\r
- */\r
-\r
-package jp.sourceforge.jindolf;\r
-\r
-import java.awt.Component;\r
-import java.awt.GridBagConstraints;\r
-import java.awt.GridBagLayout;\r
-import java.awt.Insets;\r
-import java.io.BufferedOutputStream;\r
-import java.io.File;\r
-import java.io.FileNotFoundException;\r
-import java.io.FileOutputStream;\r
-import java.io.IOException;\r
-import java.io.OutputStream;\r
-import java.io.OutputStreamWriter;\r
-import java.io.Writer;\r
-import java.nio.charset.Charset;\r
-import java.nio.charset.CharsetEncoder;\r
-import java.util.LinkedList;\r
-import java.util.List;\r
-import javax.swing.BorderFactory;\r
-import javax.swing.JComboBox;\r
-import javax.swing.JComponent;\r
-import javax.swing.JFileChooser;\r
-import javax.swing.JOptionPane;\r
-import javax.swing.JPanel;\r
-import javax.swing.border.Border;\r
-import javax.swing.filechooser.FileFilter;\r
-import jp.sourceforge.jindolf.corelib.TalkType;\r
-\r
-/**\r
- * 任意のPeriodの発言内容をCSVファイルへエクスポートする。\r
- * according to RFC4180 (text/csv)\r
- * @see <a href="http://www.ietf.org/rfc/rfc4180.txt">RFC4180</a>\r
- */\r
-public final class CsvExporter{\r
-\r
- private static final String[] ENCNAMES = {\r
- "UTF-8",\r
-\r
- "ISO-2022-JP",\r
- "ISO-2022-JP-2",\r
- "ISO-2022-JP-3",\r
- "ISO-2022-JP-2004",\r
-\r
- "EUC-JP",\r
- "x-euc-jp-linux",\r
- "x-eucJP-Open",\r
-\r
- "Shift_JIS",\r
- "windows-31j",\r
- "x-MS932_0213",\r
- "x-SJIS_0213",\r
- "x-PCK",\r
- };\r
- private static final String JPCHECK =\r
- "[]09AZ"\r
- + "あんアンアンゐゑヵヶヴヰヱヮ"\r
- + "亜瑤凜熙壷壺尭堯"\r
- + "峠"\r
- + "〒╋";\r
- private static final String CSVEXT = ".csv";\r
- private static final char CR = '\r';\r
- private static final char LF = '\n';\r
- private static final String CRLF = CR +""+ LF;\r
- private static final int BUFSIZ = 1024;\r
-\r
- private static final List<Charset> CHARSET_LIST = buildCharsetList();\r
- private static final FileFilter CSV_FILTER = new CsvFileFilter();\r
- private static final JComboBox encodeBox = new JComboBox();\r
- private static final JFileChooser chooser = buildChooser();\r
- // TODO staticなGUIパーツってどうなんだ…\r
-\r
- /**\r
- * Charsetが日本語エンコーダを持っているか確認する。\r
- * @param cs Charset\r
- * @return 日本語エンコーダを持っていればtrue\r
- */\r
- private static boolean hasJPencoder(Charset cs){\r
- if( ! cs.canEncode() ) return false;\r
- CharsetEncoder encoder = cs.newEncoder();\r
- try{\r
- if(encoder.canEncode(JPCHECK)) return true;\r
- }catch(Exception e){\r
- return false;\r
- // 一部JRE1.5系の「x-euc-jp-linux」エンコーディング実装には\r
- // canEncode()が例外を投げるバグがあるので、その対処。\r
- }\r
- return false;\r
- }\r
-\r
- /**\r
- * 日本語Charset一覧を生成する。\r
- * @return 日本語Charset一覧\r
- */\r
- private static List<Charset> buildCharsetList(){\r
- List<Charset> csList = new LinkedList<Charset>();\r
- for(String name : ENCNAMES){\r
- if( ! Charset.isSupported(name) ) continue;\r
- Charset cs = Charset.forName(name);\r
-\r
- if(csList.contains(cs)) continue;\r
-\r
- if( ! hasJPencoder(cs) ) continue;\r
-\r
- csList.add(cs);\r
- }\r
-\r
- Charset defcs = Charset.defaultCharset();\r
- if( defcs.name().equals("windows-31j")\r
- && Charset.isSupported("Shift_JIS") ){\r
- defcs = Charset.forName("Shift_JIS");\r
- }\r
-\r
- if( hasJPencoder(defcs) || csList.size() <= 0 ){\r
- if(csList.contains(defcs)){\r
- csList.remove(defcs);\r
- }\r
- csList.add(0, defcs);\r
- }\r
-\r
- return csList;\r
- }\r
-\r
- /**\r
- * チューザーをビルドする。\r
- * @return チューザー\r
- */\r
- private static JFileChooser buildChooser(){\r
- JFileChooser result = new JFileChooser();\r
-\r
- result.setFileSelectionMode(JFileChooser.FILES_ONLY);\r
- result.setMultiSelectionEnabled(false);\r
- result.setFileHidingEnabled(true);\r
-\r
- result.setAcceptAllFileFilterUsed(true);\r
-\r
- result.setFileFilter(CSV_FILTER);\r
-\r
- JComponent accessory = buildAccessory();\r
- result.setAccessory(accessory);\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * チューザのアクセサリを生成する。\r
- * エンコード指定のコンボボックス。\r
- * @return アクセサリ\r
- */\r
- private static JComponent buildAccessory(){\r
- for(Charset cs : CHARSET_LIST){\r
- encodeBox.addItem(cs);\r
- }\r
-\r
- Border border = BorderFactory.createTitledBorder("出力エンコード");\r
- encodeBox.setBorder(border);\r
-\r
- JPanel accessory = new JPanel();\r
- GridBagLayout layout = new GridBagLayout();\r
- GridBagConstraints constraints = new GridBagConstraints();\r
- accessory.setLayout(layout);\r
-\r
- constraints.insets = new Insets(3, 3, 3, 3);\r
- constraints.gridwidth = GridBagConstraints.REMAINDER;\r
- constraints.fill = GridBagConstraints.NONE;\r
- constraints.weightx = 0.0;\r
- constraints.weighty = 0.0;\r
- constraints.anchor = GridBagConstraints.NORTHWEST;\r
-\r
- accessory.add(encodeBox, constraints);\r
-\r
- constraints.fill = GridBagConstraints.BOTH;\r
- constraints.weightx = 1.0;\r
- constraints.weighty = 1.0;\r
-\r
- accessory.add(new JPanel(), constraints); // dummy\r
-\r
- return accessory;\r
- }\r
-\r
- /**\r
- * ファイルに書き込めない/作れないエラー用のダイアログを表示する。\r
- * @param file 書き込もうとしたファイル。\r
- */\r
- private static void writeError(File file){\r
- Component parent = null;\r
- String title = "ファイル書き込みエラー";\r
- String message = "ファイル「" + file.toString() + "」\n"\r
- +"に書き込むことができません。";\r
-\r
- JOptionPane.showMessageDialog(parent, message, title,\r
- JOptionPane.ERROR_MESSAGE );\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * ファイル上書き確認ダイアログを表示する。\r
- * @param file 上書き対象ファイル\r
- * @return 上書きOKが指示されたらtrue\r
- */\r
- private static boolean confirmOverwrite(File file){\r
- Component parent = null;\r
- String title = "上書き確認";\r
- String message = "既存のファイル「" + file.toString() + "」\n"\r
- +"を上書きしようとしています。続けますか?";\r
-\r
- int confirm = JOptionPane.showConfirmDialog(\r
- parent, message, title,\r
- JOptionPane.WARNING_MESSAGE,\r
- JOptionPane.OK_CANCEL_OPTION );\r
-\r
- if(confirm == JOptionPane.OK_OPTION) return true;\r
-\r
- return false;\r
- }\r
-\r
- /**\r
- * チューザーのタイトルを設定する。\r
- * @param period エクスポート対象の日\r
- */\r
- private static void setTitle(Period period){\r
- Village village = period.getVillage();\r
- String villageName = village.getVillageName();\r
- String title = villageName + "村 " + period.getCaption();\r
- title += "の発言をCSVファイルへエクスポートします";\r
- chooser.setDialogTitle(title);\r
- return;\r
- }\r
-\r
- /**\r
- * エクスポート先ファイルの名前を生成する。\r
- * @param period エクスポート対象の日\r
- * @return エクスポートファイル名\r
- */\r
- private static String createUniqueFileName(Period period){\r
- Village village = period.getVillage();\r
- String villageName = village.getVillageName();\r
-\r
- String base = "JIN_" + villageName;\r
-\r
- switch(period.getType()){\r
- case PROLOGUE:\r
- base += "_Prologue";\r
- break;\r
- case EPILOGUE:\r
- base += "_Epilogue";\r
- break;\r
- case PROGRESS:\r
- base += "_Day";\r
- base += period.getDay();\r
- break;\r
- default:\r
- assert false;\r
- break;\r
- }\r
-\r
- File saveFile;\r
- String csvName;\r
- int serial = 1;\r
- do{\r
- csvName = base;\r
- if(serial > 1){\r
- csvName += "("+ serial +")";\r
- }\r
- serial++;\r
- csvName += CSVEXT;\r
-\r
- File current = chooser.getCurrentDirectory();\r
- saveFile = new File(current, csvName);\r
- }while(saveFile.exists());\r
-\r
- return csvName;\r
- }\r
-\r
- /**\r
- * Period情報をダンプする。\r
- * @param out 格納先\r
- * @param period ダンプ対象Period\r
- * @param topicFilter 発言フィルタ\r
- * @throws java.io.IOException 出力エラー\r
- */\r
- private static void dumpPeriod(Appendable out,\r
- Period period,\r
- TopicFilter topicFilter)\r
- throws IOException{\r
- String day = String.valueOf(period.getDay());\r
-\r
- List<Topic> topicList = period.getTopicList();\r
- for(Topic topic : topicList){\r
- if( ! (topic instanceof Talk) ) continue;\r
- Talk talk = (Talk) topic;\r
- if(talk.getTalkCount() <= 0) continue;\r
-\r
- if(topicFilter.isFiltered(talk)) continue;\r
-\r
- Avatar avatar = talk.getAvatar();\r
-\r
- String name = avatar.getName();\r
- int hour = talk.getHour();\r
- int minute = talk.getMinute();\r
- TalkType type = talk.getTalkType();\r
- CharSequence dialog = talk.getDialog();\r
-\r
- out.append(name).append(',');\r
-\r
- out.append(day).append(',');\r
-\r
- out.append(Character.forDigit(hour / 10, 10));\r
- out.append(Character.forDigit(hour % 10, 10));\r
- out.append(':');\r
- out.append(Character.forDigit(minute / 10, 10));\r
- out.append(Character.forDigit(minute % 10, 10));\r
- out.append(',');\r
-\r
- switch(type){\r
- case PUBLIC: out.append("say"); break;\r
- case PRIVATE: out.append("think"); break;\r
- case WOLFONLY: out.append("whisper"); break;\r
- case GRAVE: out.append("groan"); break;\r
- default: assert false; break;\r
- }\r
- out.append(',');\r
-\r
- escapeCSV(out, dialog);\r
- out.append(CRLF);\r
- }\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * ダイアログ操作に従いPeriodをエクスポートする。\r
- * @param period エクスポート対象のPeriod\r
- * @param topicFilter 発言フィルタ\r
- * @return エクスポートしたファイル\r
- */\r
- public static File exportPeriod(Period period, TopicFilter topicFilter){\r
- setTitle(period);\r
-\r
- String uniqName = createUniqueFileName(period);\r
- File uniqFile = new File(uniqName);\r
- chooser.setSelectedFile(uniqFile);\r
-\r
- int result = chooser.showSaveDialog(null);\r
-\r
- if(result != JFileChooser.APPROVE_OPTION) return null;\r
-\r
- File selected = chooser.getSelectedFile();\r
-\r
- if( ! hasExtent(selected.getName()) ){\r
- FileFilter filter = chooser.getFileFilter();\r
- if(filter == CSV_FILTER){\r
- String path = selected.getPath();\r
- path += CSVEXT;\r
- selected = new File(path);\r
- }\r
- }\r
-\r
- if(selected.exists()){\r
- if( ! selected.isFile() || ! selected.canWrite() ){\r
- writeError(selected);\r
- return null;\r
- }\r
- boolean confirmed = confirmOverwrite(selected);\r
- if( ! confirmed ) return null;\r
- }else{\r
- boolean created;\r
- try{\r
- created = selected.createNewFile();\r
- }catch(IOException e){\r
- writeError(selected);\r
- return null;\r
- }\r
-\r
- if( ! created ){\r
- boolean confirmed = confirmOverwrite(selected);\r
- if( ! confirmed ) return null;\r
- }\r
- }\r
-\r
- OutputStream os;\r
- try{\r
- os = new FileOutputStream(selected);\r
- }catch(FileNotFoundException e){\r
- writeError(selected);\r
- return null;\r
- }\r
- os = new BufferedOutputStream(os, BUFSIZ);\r
-\r
- Charset cs = (Charset)( encodeBox.getSelectedItem() );\r
-\r
- boolean hasIOError = false;\r
- Writer writer = new OutputStreamWriter(os, cs);\r
- try{\r
- dumpPeriod(writer, period, topicFilter);\r
- }catch(IOException e){\r
- hasIOError = true;\r
- }finally{\r
- try{\r
- writer.close();\r
- }catch(IOException e){\r
- hasIOError = true;\r
- }\r
- }\r
- if(hasIOError) writeError(selected);\r
-\r
- return selected;\r
- }\r
-\r
- /**\r
- * CSV用のエスケープシーケンス処理を行う。\r
- * RFC4180準拠。\r
- * @param app 格納先\r
- * @param seq エスケープシーケンス対象\r
- * @return appと同じもの\r
- * @throws java.io.IOException 出力エラー\r
- */\r
- public static Appendable escapeCSV(Appendable app, CharSequence seq)\r
- throws IOException{\r
- app.append('"');\r
-\r
- int length = seq.length();\r
-\r
- for(int pos = 0; pos < length; pos++){\r
- char ch = seq.charAt(pos);\r
- switch(ch){\r
- case '"':\r
- app.append("\"\"");\r
- continue;\r
- case '\n':\r
- app.append(CRLF);\r
- continue;\r
- default:\r
- app.append(ch);\r
- break;\r
- }\r
- }\r
-\r
- app.append('"');\r
-\r
- return app;\r
- }\r
-\r
- /**\r
- * ファイル名が任意の拡張子を持つか判定する。\r
- * 英字大小は同一視される。\r
- * 拡張子の前は必ず一文字以上何かがなければならない。\r
- * @param filename ファイル名\r
- * @param extent '.'で始まる拡張子文字列\r
- * @return 指定された拡張子を持つならtrue\r
- */\r
- public static boolean hasExtent(CharSequence filename,\r
- CharSequence extent ){\r
- int flength = filename.length();\r
- int elength = extent .length();\r
- if(elength < 2) return false;\r
- if(flength <= elength) return false;\r
-\r
- if(filename.charAt(0) == '.') return false;\r
-\r
- int offset = flength - elength;\r
- assert offset > 0;\r
-\r
- for(int pos = 0; pos < elength; pos++){\r
- char ech = Character.toLowerCase(extent .charAt(pos ));\r
- char fch = Character.toLowerCase(filename.charAt(pos + offset));\r
- if(fch != ech) return false;\r
- }\r
-\r
- return true;\r
- }\r
-\r
- /**\r
- * パス名抜きのファイル名が拡張子を持つか判定する。\r
- * 先頭が.で始まるファイル名は拡張子を持たない。\r
- * 末尾が.で終わるファイル名は拡張子を持たない。\r
- * それ以外の.を含むファイル名は拡張子を持つとみなす。\r
- * @param filename パス名抜きのファイル名\r
- * @return 拡張子を持っていればtrue\r
- */\r
- public static boolean hasExtent(CharSequence filename){\r
- int length = filename.length();\r
- if(length < 3) return false;\r
-\r
- if(filename.charAt(0) == '.') return false;\r
- int lastPos = length - 1;\r
- if(filename.charAt(lastPos) == '.') return false;\r
-\r
- for(int pos = 1; pos <= lastPos - 1; pos++){\r
- char ch = filename.charAt(pos);\r
- if(ch == '.') return true;\r
- }\r
-\r
- return false;\r
- }\r
-\r
- /**\r
- * 隠しコンストラクタ。\r
- */\r
- private CsvExporter(){\r
- assert false;\r
- throw new AssertionError();\r
- }\r
-\r
- /**\r
- * CSVファイル表示用フィルタ。\r
- * 名前が「*.csv」の通常ファイルとディレクトリのみ表示させる。\r
- * ※ 表示の可否を問うものであって、選択の可否を問うものではない。\r
- */\r
- private static class CsvFileFilter extends FileFilter{\r
-\r
- /**\r
- * コンストラクタ。\r
- */\r
- public CsvFileFilter(){\r
- super();\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param file {@inheritDoc}\r
- * @return {@inheritDoc}\r
- */\r
- public boolean accept(File file){\r
- if(file.isDirectory()) return true;\r
- if( ! file.isFile() ) return false;\r
-\r
- if( ! hasExtent(file.getName(), CSVEXT) ) return false;\r
-\r
- return true;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @return {@inheritDoc}\r
- */\r
- public String getDescription(){\r
- return "CSVファイル (*.csv)";\r
- }\r
- }\r
-\r
- // TODO SecurityExceptionの捕捉\r
- // 書き込み中のファイルロック\r
-}\r
+/*
+ * CSV file exporter
+ *
+ * License : The MIT License
+ * Copyright(c) 2009 olyutorskii
+ */
+
+package jp.sfjp.jindolf.dxchg;
+
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.util.LinkedList;
+import java.util.List;
+import javax.swing.BorderFactory;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.border.Border;
+import javax.swing.filechooser.FileFilter;
+import jp.sfjp.jindolf.data.Avatar;
+import jp.sfjp.jindolf.data.Period;
+import jp.sfjp.jindolf.data.Talk;
+import jp.sfjp.jindolf.data.Topic;
+import jp.sfjp.jindolf.data.Village;
+import jp.sfjp.jindolf.view.TopicFilter;
+import jp.sourceforge.jindolf.corelib.TalkType;
+
+/**
+ * 任意のPeriodの発言内容をCSVファイルへエクスポートする。
+ * according to RFC4180 (text/csv)
+ * @see <a href="http://www.ietf.org/rfc/rfc4180.txt">RFC4180</a>
+ */
+public final class CsvExporter{
+
+ private static final String[] ENCNAMES = {
+ "UTF-8",
+
+ "ISO-2022-JP",
+ "ISO-2022-JP-2",
+ "ISO-2022-JP-3",
+ "ISO-2022-JP-2004",
+
+ "EUC-JP",
+ "x-euc-jp-linux",
+ "x-eucJP-Open",
+
+ "Shift_JIS",
+ "windows-31j",
+ "x-MS932_0213",
+ "x-SJIS_0213",
+ "x-PCK",
+ };
+ private static final String JPCHECK =
+ "[]09AZ"
+ + "あんアンアンゐゑヵヶヴヰヱヮ"
+ + "亜瑤凜熙壷壺尭堯"
+ + "峠"
+ + "〒╋";
+ private static final String CSVEXT = ".csv";
+ private static final char CR = '\r';
+ private static final char LF = '\n';
+ private static final String CRLF = CR +""+ LF;
+ private static final int BUFSIZ = 1024;
+
+ private static final List<Charset> CHARSET_LIST = buildCharsetList();
+ private static final FileFilter CSV_FILTER = new CsvFileFilter();
+ private static final JComboBox<Charset> encodeBox = new JComboBox<>();
+ private static final JFileChooser chooser = buildChooser();
+ // TODO staticなGUIパーツってどうなんだ…
+
+
+ /**
+ * 隠しコンストラクタ。
+ */
+ private CsvExporter(){
+ assert false;
+ throw new AssertionError();
+ }
+
+
+ /**
+ * Charsetが日本語エンコーダを持っているか確認する。
+ * @param cs Charset
+ * @return 日本語エンコーダを持っていればtrue
+ */
+ private static boolean hasJPencoder(Charset cs){
+ if( ! cs.canEncode() ) return false;
+ CharsetEncoder encoder = cs.newEncoder();
+ try{
+ if(encoder.canEncode(JPCHECK)) return true;
+ }catch(Exception e){
+ return false;
+ // 一部JRE1.5系の「x-euc-jp-linux」エンコーディング実装には
+ // canEncode()が例外を投げるバグがあるので、その対処。
+ }
+ return false;
+ }
+
+ /**
+ * 日本語Charset一覧を生成する。
+ * @return 日本語Charset一覧
+ */
+ private static List<Charset> buildCharsetList(){
+ List<Charset> csList = new LinkedList<>();
+ for(String name : ENCNAMES){
+ if( ! Charset.isSupported(name) ) continue;
+ Charset cs = Charset.forName(name);
+
+ if(csList.contains(cs)) continue;
+
+ if( ! hasJPencoder(cs) ) continue;
+
+ csList.add(cs);
+ }
+
+ Charset defcs = Charset.defaultCharset();
+ if( defcs.name().equals("windows-31j")
+ && Charset.isSupported("Shift_JIS") ){
+ defcs = Charset.forName("Shift_JIS");
+ }
+
+ if( hasJPencoder(defcs) || csList.size() <= 0 ){
+ if(csList.contains(defcs)){
+ csList.remove(defcs);
+ }
+ csList.add(0, defcs);
+ }
+
+ return csList;
+ }
+
+ /**
+ * チューザーをビルドする。
+ * @return チューザー
+ */
+ private static JFileChooser buildChooser(){
+ JFileChooser result = new JFileChooser();
+
+ result.setFileSelectionMode(JFileChooser.FILES_ONLY);
+ result.setMultiSelectionEnabled(false);
+ result.setFileHidingEnabled(true);
+
+ result.setAcceptAllFileFilterUsed(true);
+
+ result.setFileFilter(CSV_FILTER);
+
+ JComponent accessory = buildAccessory();
+ result.setAccessory(accessory);
+
+ return result;
+ }
+
+ /**
+ * チューザのアクセサリを生成する。
+ * エンコード指定のコンボボックス。
+ * @return アクセサリ
+ */
+ private static JComponent buildAccessory(){
+ for(Charset cs : CHARSET_LIST){
+ encodeBox.addItem(cs);
+ }
+
+ Border border = BorderFactory.createTitledBorder("出力エンコード");
+ encodeBox.setBorder(border);
+
+ JPanel accessory = new JPanel();
+ GridBagLayout layout = new GridBagLayout();
+ GridBagConstraints constraints = new GridBagConstraints();
+ accessory.setLayout(layout);
+
+ constraints.insets = new Insets(3, 3, 3, 3);
+ constraints.gridwidth = GridBagConstraints.REMAINDER;
+ constraints.fill = GridBagConstraints.NONE;
+ constraints.weightx = 0.0;
+ constraints.weighty = 0.0;
+ constraints.anchor = GridBagConstraints.NORTHWEST;
+
+ accessory.add(encodeBox, constraints);
+
+ constraints.fill = GridBagConstraints.BOTH;
+ constraints.weightx = 1.0;
+ constraints.weighty = 1.0;
+
+ accessory.add(new JPanel(), constraints); // dummy
+
+ return accessory;
+ }
+
+ /**
+ * ファイルに書き込めない/作れないエラー用のダイアログを表示する。
+ * @param file 書き込もうとしたファイル。
+ */
+ private static void writeError(File file){
+ Component parent = null;
+ String title = "ファイル書き込みエラー";
+ String message = "ファイル「" + file.toString() + "」\n"
+ +"に書き込むことができません。";
+
+ JOptionPane.showMessageDialog(parent, message, title,
+ JOptionPane.ERROR_MESSAGE );
+
+ return;
+ }
+
+ /**
+ * ファイル上書き確認ダイアログを表示する。
+ * @param file 上書き対象ファイル
+ * @return 上書きOKが指示されたらtrue
+ */
+ private static boolean confirmOverwrite(File file){
+ Component parent = null;
+ String title = "上書き確認";
+ String message = "既存のファイル「" + file.toString() + "」\n"
+ +"を上書きしようとしています。続けますか?";
+
+ int confirm = JOptionPane.showConfirmDialog(
+ parent, message, title,
+ JOptionPane.WARNING_MESSAGE,
+ JOptionPane.OK_CANCEL_OPTION );
+
+ if(confirm == JOptionPane.OK_OPTION) return true;
+
+ return false;
+ }
+
+ /**
+ * チューザーのタイトルを設定する。
+ * @param period エクスポート対象の日
+ */
+ private static void setTitle(Period period){
+ Village village = period.getVillage();
+ String villageName = village.getVillageName();
+ String title = villageName + "村 " + period.getCaption();
+ title += "の発言をCSVファイルへエクスポートします";
+ chooser.setDialogTitle(title);
+ return;
+ }
+
+ /**
+ * エクスポート先ファイルの名前を生成する。
+ * @param period エクスポート対象の日
+ * @return エクスポートファイル名
+ */
+ private static String createUniqueFileName(Period period){
+ Village village = period.getVillage();
+ String villageName = village.getVillageName();
+
+ String base = "JIN_" + villageName;
+
+ switch(period.getType()){
+ case PROLOGUE:
+ base += "_Prologue";
+ break;
+ case EPILOGUE:
+ base += "_Epilogue";
+ break;
+ case PROGRESS:
+ base += "_Day";
+ base += period.getDay();
+ break;
+ default:
+ assert false;
+ break;
+ }
+
+ File saveFile;
+ String csvName;
+ int serial = 1;
+ do{
+ csvName = base;
+ if(serial > 1){
+ csvName += "("+ serial +")";
+ }
+ serial++;
+ csvName += CSVEXT;
+
+ File current = chooser.getCurrentDirectory();
+ saveFile = new File(current, csvName);
+ }while(saveFile.exists());
+
+ return csvName;
+ }
+
+ /**
+ * Period情報をダンプする。
+ * @param out 格納先
+ * @param period ダンプ対象Period
+ * @param topicFilter 発言フィルタ
+ * @throws java.io.IOException 出力エラー
+ */
+ private static void dumpPeriod(Appendable out,
+ Period period,
+ TopicFilter topicFilter)
+ throws IOException{
+ String day = String.valueOf(period.getDay());
+
+ List<Topic> topicList = period.getTopicList();
+ for(Topic topic : topicList){
+ if( ! (topic instanceof Talk) ) continue;
+ Talk talk = (Talk) topic;
+ if(talk.getTalkCount() <= 0) continue;
+
+ if(topicFilter.isFiltered(talk)) continue;
+
+ Avatar avatar = talk.getAvatar();
+
+ String name = avatar.getName();
+ int hour = talk.getHour();
+ int minute = talk.getMinute();
+ TalkType type = talk.getTalkType();
+ CharSequence dialog = talk.getDialog();
+
+ out.append(name).append(',');
+
+ out.append(day).append(',');
+
+ out.append(Character.forDigit(hour / 10, 10));
+ out.append(Character.forDigit(hour % 10, 10));
+ out.append(':');
+ out.append(Character.forDigit(minute / 10, 10));
+ out.append(Character.forDigit(minute % 10, 10));
+ out.append(',');
+
+ switch(type){
+ case PUBLIC: out.append("say"); break;
+ case PRIVATE: out.append("think"); break;
+ case WOLFONLY: out.append("whisper"); break;
+ case GRAVE: out.append("groan"); break;
+ default: assert false; break;
+ }
+ out.append(',');
+
+ escapeCSV(out, dialog);
+ out.append(CRLF);
+ }
+
+ return;
+ }
+
+ /**
+ * ダイアログ操作に従いPeriodをエクスポートする。
+ * @param period エクスポート対象のPeriod
+ * @param topicFilter 発言フィルタ
+ * @return エクスポートしたファイル
+ */
+ public static File exportPeriod(Period period, TopicFilter topicFilter){
+ setTitle(period);
+
+ String uniqName = createUniqueFileName(period);
+ File uniqFile = new File(uniqName);
+ chooser.setSelectedFile(uniqFile);
+
+ int result = chooser.showSaveDialog(null);
+
+ if(result != JFileChooser.APPROVE_OPTION) return null;
+
+ File selected = chooser.getSelectedFile();
+
+ if( ! hasExtent(selected.getName()) ){
+ FileFilter filter = chooser.getFileFilter();
+ if(filter == CSV_FILTER){
+ String path = selected.getPath();
+ path += CSVEXT;
+ selected = new File(path);
+ }
+ }
+
+ if(selected.exists()){
+ if( ! selected.isFile() || ! selected.canWrite() ){
+ writeError(selected);
+ return null;
+ }
+ boolean confirmed = confirmOverwrite(selected);
+ if( ! confirmed ) return null;
+ }else{
+ boolean created;
+ try{
+ created = selected.createNewFile();
+ }catch(IOException e){
+ writeError(selected);
+ return null;
+ }
+
+ if( ! created ){
+ boolean confirmed = confirmOverwrite(selected);
+ if( ! confirmed ) return null;
+ }
+ }
+
+ OutputStream os;
+ try{
+ os = new FileOutputStream(selected);
+ }catch(FileNotFoundException e){
+ writeError(selected);
+ return null;
+ }
+ os = new BufferedOutputStream(os, BUFSIZ);
+
+ Charset cs = (Charset) ( encodeBox.getSelectedItem() );
+
+ boolean hasIOError = false;
+ Writer writer = new OutputStreamWriter(os, cs);
+ try{
+ dumpPeriod(writer, period, topicFilter);
+ }catch(IOException e){
+ hasIOError = true;
+ }finally{
+ try{
+ writer.close();
+ }catch(IOException e){
+ hasIOError = true;
+ }
+ }
+ if(hasIOError) writeError(selected);
+
+ return selected;
+ }
+
+ /**
+ * CSV用のエスケープシーケンス処理を行う。
+ * RFC4180準拠。
+ * @param app 格納先
+ * @param seq エスケープシーケンス対象
+ * @return appと同じもの
+ * @throws java.io.IOException 出力エラー
+ */
+ public static Appendable escapeCSV(Appendable app, CharSequence seq)
+ throws IOException{
+ app.append('"');
+
+ int length = seq.length();
+
+ for(int pos = 0; pos < length; pos++){
+ char ch = seq.charAt(pos);
+ switch(ch){
+ case '"':
+ app.append("\"\"");
+ continue;
+ case '\n':
+ app.append(CRLF);
+ continue;
+ default:
+ app.append(ch);
+ break;
+ }
+ }
+
+ app.append('"');
+
+ return app;
+ }
+
+ /**
+ * ファイル名が任意の拡張子を持つか判定する。
+ * 英字大小は同一視される。
+ * 拡張子の前は必ず一文字以上何かがなければならない。
+ * @param filename ファイル名
+ * @param extent '.'で始まる拡張子文字列
+ * @return 指定された拡張子を持つならtrue
+ */
+ public static boolean hasExtent(CharSequence filename,
+ CharSequence extent ){
+ int flength = filename.length();
+ int elength = extent .length();
+ if(elength < 2) return false;
+ if(flength <= elength) return false;
+
+ if(filename.charAt(0) == '.') return false;
+
+ int offset = flength - elength;
+ assert offset > 0;
+
+ for(int pos = 0; pos < elength; pos++){
+ char ech = Character.toLowerCase(extent .charAt(pos ));
+ char fch = Character.toLowerCase(filename.charAt(pos + offset));
+ if(fch != ech) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * パス名抜きのファイル名が拡張子を持つか判定する。
+ * 先頭が.で始まるファイル名は拡張子を持たない。
+ * 末尾が.で終わるファイル名は拡張子を持たない。
+ * それ以外の.を含むファイル名は拡張子を持つとみなす。
+ * @param filename パス名抜きのファイル名
+ * @return 拡張子を持っていればtrue
+ */
+ public static boolean hasExtent(CharSequence filename){
+ int length = filename.length();
+ if(length < 3) return false;
+
+ if(filename.charAt(0) == '.') return false;
+ int lastPos = length - 1;
+ if(filename.charAt(lastPos) == '.') return false;
+
+ for(int pos = 1; pos <= lastPos - 1; pos++){
+ char ch = filename.charAt(pos);
+ if(ch == '.') return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * CSVファイル表示用フィルタ。
+ * 名前が「*.csv」の通常ファイルとディレクトリのみ表示させる。
+ * ※ 表示の可否を問うものであって、選択の可否を問うものではない。
+ */
+ private static class CsvFileFilter extends FileFilter{
+
+ /**
+ * コンストラクタ。
+ */
+ public CsvFileFilter(){
+ super();
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param file {@inheritDoc}
+ * @return {@inheritDoc}
+ */
+ @Override
+ public boolean accept(File file){
+ if(file.isDirectory()) return true;
+ if( ! file.isFile() ) return false;
+
+ if( ! hasExtent(file.getName(), CSVEXT) ) return false;
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @return {@inheritDoc}
+ */
+ @Override
+ public String getDescription(){
+ return "CSVファイル (*.csv)";
+ }
+ }
+
+ // TODO SecurityExceptionの捕捉
+ // 書き込み中のファイルロック
+}