--- /dev/null
+/*\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