OSDN Git Service

from subversion repository
[jindolf/Jindolf.git] / src / main / java / jp / sourceforge / jindolf / CsvExporter.java
1 /*\r
2  * CSV file exporter\r
3  *\r
4  * Copyright(c) 2009 olyutorskii\r
5  * $Id: CsvExporter.java 953 2009-12-06 16:42:14Z olyutorskii $\r
6  */\r
7 \r
8 package jp.sourceforge.jindolf;\r
9 \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
35 \r
36 /**\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
40  */\r
41 public final class CsvExporter{\r
42 \r
43     private static final String[] ENCNAMES = {\r
44         "UTF-8",\r
45 \r
46         "ISO-2022-JP",\r
47         "ISO-2022-JP-2",\r
48         "ISO-2022-JP-3",\r
49         "ISO-2022-JP-2004",\r
50 \r
51         "EUC-JP",\r
52         "x-euc-jp-linux",\r
53         "x-eucJP-Open",\r
54 \r
55         "Shift_JIS",\r
56         "windows-31j",\r
57         "x-MS932_0213",\r
58         "x-SJIS_0213",\r
59         "x-PCK",\r
60     };\r
61     private static final String JPCHECK =\r
62           "[]09AZ"\r
63         + "あんアンアンゐゑヵヶヴヰヱヮ"\r
64         + "亜瑤凜熙壷壺尭堯"\r
65         + "峠"\r
66         + "〒╋";\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
72 \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
78 \r
79     /**\r
80      * Charsetが日本語エンコーダを持っているか確認する。\r
81      * @param cs Charset\r
82      * @return 日本語エンコーダを持っていればtrue\r
83      */\r
84     private static boolean hasJPencoder(Charset cs){\r
85         if( ! cs.canEncode() ) return false;\r
86         CharsetEncoder encoder = cs.newEncoder();\r
87         try{\r
88             if(encoder.canEncode(JPCHECK)) return true;\r
89         }catch(Exception e){\r
90             return false;\r
91             // 一部JRE1.5系の「x-euc-jp-linux」エンコーディング実装には\r
92             // canEncode()が例外を投げるバグがあるので、その対処。\r
93         }\r
94         return false;\r
95     }\r
96 \r
97     /**\r
98      * 日本語Charset一覧を生成する。\r
99      * @return 日本語Charset一覧\r
100      */\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
106 \r
107             if(csList.contains(cs)) continue;\r
108 \r
109             if( ! hasJPencoder(cs) ) continue;\r
110 \r
111             csList.add(cs);\r
112         }\r
113 \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
118         }\r
119 \r
120         if( hasJPencoder(defcs) || csList.size() <= 0 ){\r
121             if(csList.contains(defcs)){\r
122                csList.remove(defcs);\r
123             }\r
124             csList.add(0, defcs);\r
125         }\r
126 \r
127         return csList;\r
128     }\r
129 \r
130     /**\r
131      * チューザーをビルドする。\r
132      * @return チューザー\r
133      */\r
134     private static JFileChooser buildChooser(){\r
135         JFileChooser result = new JFileChooser();\r
136 \r
137         result.setFileSelectionMode(JFileChooser.FILES_ONLY);\r
138         result.setMultiSelectionEnabled(false);\r
139         result.setFileHidingEnabled(true);\r
140 \r
141         result.setAcceptAllFileFilterUsed(true);\r
142 \r
143         result.setFileFilter(CSV_FILTER);\r
144 \r
145         JComponent accessory = buildAccessory();\r
146         result.setAccessory(accessory);\r
147 \r
148         return result;\r
149     }\r
150 \r
151     /**\r
152      * チューザのアクセサリを生成する。\r
153      * エンコード指定のコンボボックス。\r
154      * @return アクセサリ\r
155      */\r
156     private static JComponent buildAccessory(){\r
157         for(Charset cs : CHARSET_LIST){\r
158             encodeBox.addItem(cs);\r
159         }\r
160 \r
161         Border border = BorderFactory.createTitledBorder("出力エンコード");\r
162         encodeBox.setBorder(border);\r
163 \r
164         JPanel accessory = new JPanel();\r
165         GridBagLayout layout = new GridBagLayout();\r
166         GridBagConstraints constraints = new GridBagConstraints();\r
167         accessory.setLayout(layout);\r
168 \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
175 \r
176         accessory.add(encodeBox, constraints);\r
177 \r
178         constraints.fill = GridBagConstraints.BOTH;\r
179         constraints.weightx = 1.0;\r
180         constraints.weighty = 1.0;\r
181 \r
182         accessory.add(new JPanel(), constraints);  // dummy\r
183 \r
184         return accessory;\r
185     }\r
186 \r
187     /**\r
188      * ファイルに書き込めない/作れないエラー用のダイアログを表示する。\r
189      * @param file 書き込もうとしたファイル。\r
190      */\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
195                         +"に書き込むことができません。";\r
196 \r
197         JOptionPane.showMessageDialog(parent, message, title,\r
198                                       JOptionPane.ERROR_MESSAGE );\r
199 \r
200         return;\r
201     }\r
202 \r
203     /**\r
204      * ファイル上書き確認ダイアログを表示する。\r
205      * @param file 上書き対象ファイル\r
206      * @return 上書きOKが指示されたらtrue\r
207      */\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
213 \r
214         int confirm = JOptionPane.showConfirmDialog(\r
215                 parent, message, title,\r
216                 JOptionPane.WARNING_MESSAGE,\r
217                 JOptionPane.OK_CANCEL_OPTION );\r
218 \r
219         if(confirm == JOptionPane.OK_OPTION) return true;\r
220 \r
221         return false;\r
222     }\r
223 \r
224     /**\r
225      * チューザーのタイトルを設定する。\r
226      * @param period エクスポート対象の日\r
227      */\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
234         return;\r
235     }\r
236 \r
237     /**\r
238      * エクスポート先ファイルの名前を生成する。\r
239      * @param period エクスポート対象の日\r
240      * @return エクスポートファイル名\r
241      */\r
242     private static String createUniqueFileName(Period period){\r
243         Village village = period.getVillage();\r
244         String villageName = village.getVillageName();\r
245 \r
246         String base = "JIN_" + villageName;\r
247 \r
248         switch(period.getType()){\r
249         case PROLOGUE:\r
250             base += "_Prologue";\r
251             break;\r
252         case EPILOGUE:\r
253             base += "_Epilogue";\r
254             break;\r
255         case PROGRESS:\r
256             base += "_Day";\r
257             base += period.getDay();\r
258             break;\r
259         default:\r
260             assert false;\r
261             break;\r
262         }\r
263 \r
264         File saveFile;\r
265         String csvName;\r
266         int serial = 1;\r
267         do{\r
268             csvName = base;\r
269             if(serial > 1){\r
270                 csvName += "("+ serial +")";\r
271             }\r
272             serial++;\r
273             csvName += CSVEXT;\r
274 \r
275             File current = chooser.getCurrentDirectory();\r
276             saveFile = new File(current, csvName);\r
277         }while(saveFile.exists());\r
278 \r
279         return csvName;\r
280     }\r
281 \r
282     /**\r
283      * Period情報をダンプする。\r
284      * @param out 格納先\r
285      * @param period ダンプ対象Period\r
286      * @param topicFilter 発言フィルタ\r
287      * @throws java.io.IOException 出力エラー\r
288      */\r
289     private static void dumpPeriod(Appendable out,\r
290                                     Period period,\r
291                                     TopicFilter topicFilter)\r
292             throws IOException{\r
293         String day = String.valueOf(period.getDay());\r
294 \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
300 \r
301             if(topicFilter.isFiltered(talk)) continue;\r
302 \r
303             Avatar avatar = talk.getAvatar();\r
304 \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
310 \r
311             out.append(name).append(',');\r
312 \r
313             out.append(day).append(',');\r
314 \r
315             out.append(Character.forDigit(hour / 10, 10));\r
316             out.append(Character.forDigit(hour % 10, 10));\r
317             out.append(':');\r
318             out.append(Character.forDigit(minute / 10, 10));\r
319             out.append(Character.forDigit(minute % 10, 10));\r
320             out.append(',');\r
321 \r
322             switch(type){\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
328             }\r
329             out.append(',');\r
330 \r
331             escapeCSV(out, dialog);\r
332             out.append(CRLF);\r
333         }\r
334 \r
335         return;\r
336     }\r
337 \r
338     /**\r
339      * ダイアログ操作に従いPeriodをエクスポートする。\r
340      * @param period エクスポート対象のPeriod\r
341      * @param topicFilter 発言フィルタ\r
342      * @return エクスポートしたファイル\r
343      */\r
344     public static File exportPeriod(Period period, TopicFilter topicFilter){\r
345         setTitle(period);\r
346 \r
347         String uniqName = createUniqueFileName(period);\r
348         File uniqFile = new File(uniqName);\r
349         chooser.setSelectedFile(uniqFile);\r
350 \r
351         int result = chooser.showSaveDialog(null);\r
352 \r
353         if(result != JFileChooser.APPROVE_OPTION) return null;\r
354 \r
355         File selected = chooser.getSelectedFile();\r
356 \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
361                 path += CSVEXT;\r
362                 selected = new File(path);\r
363             }\r
364         }\r
365 \r
366         if(selected.exists()){\r
367             if( ! selected.isFile() || ! selected.canWrite() ){\r
368                 writeError(selected);\r
369                 return null;\r
370             }\r
371             boolean confirmed = confirmOverwrite(selected);\r
372             if( ! confirmed ) return null;\r
373         }else{\r
374             boolean created;\r
375             try{\r
376                 created = selected.createNewFile();\r
377             }catch(IOException e){\r
378                 writeError(selected);\r
379                 return null;\r
380             }\r
381 \r
382             if( ! created ){\r
383                 boolean confirmed = confirmOverwrite(selected);\r
384                 if( ! confirmed ) return null;\r
385             }\r
386         }\r
387 \r
388         OutputStream os;\r
389         try{\r
390             os = new FileOutputStream(selected);\r
391         }catch(FileNotFoundException e){\r
392             writeError(selected);\r
393             return null;\r
394         }\r
395         os = new BufferedOutputStream(os, BUFSIZ);\r
396 \r
397         Charset cs = (Charset)( encodeBox.getSelectedItem() );\r
398 \r
399         boolean hasIOError = false;\r
400         Writer writer = new OutputStreamWriter(os, cs);\r
401         try{\r
402             dumpPeriod(writer, period, topicFilter);\r
403         }catch(IOException e){\r
404             hasIOError = true;\r
405         }finally{\r
406             try{\r
407                 writer.close();\r
408             }catch(IOException e){\r
409                 hasIOError = true;\r
410             }\r
411         }\r
412         if(hasIOError) writeError(selected);\r
413 \r
414         return selected;\r
415     }\r
416 \r
417     /**\r
418      * CSV用のエスケープシーケンス処理を行う。\r
419      * RFC4180準拠。\r
420      * @param app 格納先\r
421      * @param seq エスケープシーケンス対象\r
422      * @return appと同じもの\r
423      * @throws java.io.IOException 出力エラー\r
424      */\r
425     public static Appendable escapeCSV(Appendable app, CharSequence seq)\r
426             throws IOException{\r
427         app.append('"');\r
428 \r
429         int length = seq.length();\r
430 \r
431         for(int pos = 0; pos < length; pos++){\r
432             char ch = seq.charAt(pos);\r
433             switch(ch){\r
434             case '"':\r
435                 app.append("\"\"");\r
436                 continue;\r
437             case '\n':\r
438                 app.append(CRLF);\r
439                 continue;\r
440             default:\r
441                 app.append(ch);\r
442                 break;\r
443             }\r
444         }\r
445 \r
446         app.append('"');\r
447 \r
448         return app;\r
449     }\r
450 \r
451     /**\r
452      * ファイル名が任意の拡張子を持つか判定する。\r
453      * 英字大小は同一視される。\r
454      * 拡張子の前は必ず一文字以上何かがなければならない。\r
455      * @param filename ファイル名\r
456      * @param extent '.'で始まる拡張子文字列\r
457      * @return 指定された拡張子を持つならtrue\r
458      */\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
465 \r
466         if(filename.charAt(0) == '.') return false;\r
467 \r
468         int offset = flength - elength;\r
469         assert offset > 0;\r
470 \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
475         }\r
476 \r
477         return true;\r
478     }\r
479 \r
480     /**\r
481      * パス名抜きのファイル名が拡張子を持つか判定する。\r
482      * 先頭が.で始まるファイル名は拡張子を持たない。\r
483      * 末尾が.で終わるファイル名は拡張子を持たない。\r
484      * それ以外の.を含むファイル名は拡張子を持つとみなす。\r
485      * @param filename パス名抜きのファイル名\r
486      * @return 拡張子を持っていればtrue\r
487      */\r
488     public static boolean hasExtent(CharSequence filename){\r
489         int length = filename.length();\r
490         if(length < 3) return false;\r
491 \r
492         if(filename.charAt(0) == '.') return false;\r
493         int lastPos = length - 1;\r
494         if(filename.charAt(lastPos) == '.') return false;\r
495 \r
496         for(int pos = 1; pos <= lastPos - 1; pos++){\r
497             char ch = filename.charAt(pos);\r
498             if(ch == '.') return true;\r
499         }\r
500 \r
501         return false;\r
502     }\r
503 \r
504     /**\r
505      * 隠しコンストラクタ。\r
506      */\r
507     private CsvExporter(){\r
508         assert false;\r
509         throw new AssertionError();\r
510     }\r
511 \r
512     /**\r
513      * CSVファイル表示用フィルタ。\r
514      * 名前が「*.csv」の通常ファイルとディレクトリのみ表示させる。\r
515      * ※ 表示の可否を問うものであって、選択の可否を問うものではない。\r
516      */\r
517     private static class CsvFileFilter extends FileFilter{\r
518 \r
519         /**\r
520          * コンストラクタ。\r
521          */\r
522         public CsvFileFilter(){\r
523             super();\r
524             return;\r
525         }\r
526 \r
527         /**\r
528          * {@inheritDoc}\r
529          * @param file {@inheritDoc}\r
530          * @return {@inheritDoc}\r
531          */\r
532         public boolean accept(File file){\r
533             if(file.isDirectory()) return true;\r
534             if( ! file.isFile() ) return false;\r
535 \r
536             if( ! hasExtent(file.getName(), CSVEXT) ) return false;\r
537 \r
538             return true;\r
539         }\r
540 \r
541         /**\r
542          * {@inheritDoc}\r
543          * @return {@inheritDoc}\r
544          */\r
545         public String getDescription(){\r
546             return "CSVファイル (*.csv)";\r
547         }\r
548     }\r
549 \r
550     // TODO SecurityExceptionの捕捉\r
551     // 書き込み中のファイルロック\r
552 }\r