OSDN Git Service

Maven3対応。
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / dxchg / WolfBBS.java
1 /*
2  * WolfBBS
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.dxchg;
9
10 import java.io.FileNotFoundException;
11 import java.io.IOException;
12 import java.nio.ByteBuffer;
13 import java.nio.CharBuffer;
14 import java.nio.charset.CharacterCodingException;
15 import java.nio.charset.Charset;
16 import java.nio.charset.CharsetEncoder;
17 import java.util.Collections;
18 import java.util.LinkedList;
19 import java.util.List;
20 import java.util.Locale;
21 import java.util.Properties;
22 import java.util.Set;
23 import java.util.SortedSet;
24 import java.util.TreeSet;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27 import jp.sfjp.jindolf.ResourceManager;
28 import jp.sfjp.jindolf.data.Avatar;
29 import jp.sfjp.jindolf.log.LogWrapper;
30 import jp.sourceforge.jindolf.corelib.Destiny;
31 import jp.sourceforge.jindolf.corelib.GameRole;
32
33 /**
34  * まちゅ氏運営のまとめサイト(wolfbbs)に関する諸々。
35  * PukiWikiベース。
36  * @see <a href="http://wolfbbs.jp/">まとめサイト</a>
37  * @see <a href="http://pukiwiki.sourceforge.jp/">PukiWiki</a>
38  */
39 public final class WolfBBS{
40
41     /** PukiWikiコメント行。 */
42     public static final String COMMENTLINE;
43
44     private static final String WIKICHAR = "#&[]()<>+-*:|~/,'%?";
45     private static final Pattern WIKINAME_PATTERN =
46             Pattern.compile("[A-Z][a-z]+([A-Z])[a-z]+");
47
48     private static final String FACEICONSET =
49             "resources/wolfbbs/faceIconSet.properties";
50     private static final String ORDER_PREFIX = "iconset.order.";
51     private static final List<FaceIconSet> FACEICONSET_LIST =
52             new LinkedList<FaceIconSet>();
53
54     private static final Charset CHARSET_EUC = Charset.forName("EUC-JP");
55
56     private static final String WOLFBBS_URL = "http://wolfbbs.jp/";
57
58     private static final LogWrapper LOGGER = new LogWrapper();
59
60     static{
61         try{
62             loadFaceIconSet();
63         }catch(FileNotFoundException e){
64             throw new ExceptionInInitializerError(e);
65         }
66
67         StringBuilder wikicomment = new StringBuilder();
68         wikicomment.append("// ");
69         while(wikicomment.length() < 72){
70             wikicomment.append('=');
71         }
72         wikicomment.append('\n');
73         COMMENTLINE = wikicomment.toString();
74     }
75
76
77     /**
78      * 隠しコンストラクタ。
79      */
80     private WolfBBS(){
81         assert false;
82         throw new AssertionError();
83     }
84
85
86     /**
87      * アイコンセットのロード。
88      * @throws FileNotFoundException リソースが不明
89      */
90     private static void loadFaceIconSet() throws FileNotFoundException {
91         Properties properties = ResourceManager.getProperties(FACEICONSET);
92         if(properties == null){
93             LOGGER.fatal("顔アイコンセットの読み込みに失敗しました");
94             throw new FileNotFoundException();
95         }
96
97         loadFaceIconSet(properties);
98
99         return;
100     }
101
102     /**
103      * アイコンセットのロード。
104      * @param properties プロパティ
105      * @throws FileNotFoundException リソースが不明
106      */
107     private static void loadFaceIconSet(Properties properties)
108             throws FileNotFoundException {
109         String codeCheck = properties.getProperty("codeCheck");
110         if(   codeCheck == null
111            || codeCheck.length() != 1
112            || codeCheck.charAt(0) != '\u72fc'){  // 「狼」
113             LOGGER.fatal(
114                     "顔アイコンセットプロパティファイルの"
115                     +"文字コードがおかしいようです。"
116                     +"native2ascii は正しく適用しましたか?");
117             throw new FileNotFoundException();
118         }
119
120         Set<Object> keySet = properties.keySet();
121
122         SortedSet<Integer> orderSet = new TreeSet<Integer>();
123         for(Object keyObj : keySet){
124             if(keyObj == null) continue;
125             String key = keyObj.toString();
126             if( ! key.startsWith(ORDER_PREFIX) ) continue;
127             key = key.replace(ORDER_PREFIX, "");
128             Integer order;
129             try{
130                 order = Integer.valueOf(key);
131             }catch(NumberFormatException e){
132                 continue;
133             }
134             orderSet.add(order);
135         }
136
137         for(Integer orderNum : orderSet){
138             String setName = properties.getProperty(ORDER_PREFIX + orderNum);
139             FaceIconSet iconSet = loadFaceIconSet(properties, setName);
140             FACEICONSET_LIST.add(iconSet);
141         }
142
143         return;
144     }
145
146     /**
147      * アイコンセットのロード。
148      * @param properties プロパティ
149      * @param setName アイコンセット名
150      * @return アイコンセット
151      */
152     private static FaceIconSet loadFaceIconSet(Properties properties,
153                                           String setName){
154         String author  = properties.getProperty(setName + ".author");
155         String caption = properties.getProperty(setName + ".caption");
156         String urlText = properties.getProperty(setName + ".url");
157
158         FaceIconSet iconSet = new FaceIconSet(caption, author, urlText);
159
160         List<Avatar> avatarList = Avatar.getPredefinedAvatarList();
161         for(Avatar avatar : avatarList){
162             String identifier = avatar.getIdentifier();
163             String key = setName + ".iconWiki." + identifier;
164             String wiki = properties.getProperty(key);
165             iconSet.registIconWiki(avatar, wiki);
166         }
167
168         return iconSet;
169     }
170
171     /**
172      * 顔アイコンセットのリストを取得する。
173      * @return 顔アイコンセットのリスト
174      */
175     public static List<FaceIconSet> getFaceIconSetList(){
176         List<FaceIconSet> result =
177                 Collections.unmodifiableList(FACEICONSET_LIST);
178         return result;
179     }
180
181     /**
182      * 任意の文字がWikiの特殊キャラクタか否か判定する。
183      * @param ch 文字
184      * @return 特殊キャラクタならtrue
185      */
186     public static boolean isWikiChar(char ch){
187         if(WIKICHAR.indexOf(ch) < 0) return false;
188         return true;
189     }
190
191     /**
192      * Wiki特殊文字を数値参照文字でエスケープする。
193      * @param seq Wiki特殊文字を含むかもしれない文字列。
194      * @return エスケープされた文字列
195      */
196     public static CharSequence escapeWikiChar(CharSequence seq){
197         StringBuilder result = new StringBuilder();
198
199         int seqLength = seq.length();
200         for(int pos = 0; pos < seqLength; pos++){
201             char ch = seq.charAt(pos);
202             if(isWikiChar(ch)){
203                 try{
204                     appendNumCharRef(result, ch);
205                 }catch(IOException e){
206                     assert false;
207                     return null;
208                 }
209             }else{
210                 result.append(ch);
211             }
212         }
213
214         return result;
215     }
216
217     /**
218      * WikiNameを数値参照文字でエスケープする。
219      * @param seq WikiNameを含むかもしれない文字列
220      * @return エスケープされた文字列。
221      */
222     public static CharSequence escapeWikiName(CharSequence seq){
223         StringBuilder result = null;
224         Matcher matcher = WIKINAME_PATTERN.matcher(seq);
225
226         int pos = 0;
227         while(matcher.find(pos)){
228             int matchStart = matcher.start();
229             int matchEnd   = matcher.end();
230             int capStart = matcher.start(1);
231             int capEnd   = matcher.end(1);
232
233             if(result == null) result = new StringBuilder();
234             result.append(seq, pos, matchStart);
235             result.append(seq, matchStart, capStart);
236             try{
237                 appendNumCharRef(result, seq.charAt(capStart));
238             }catch(IOException e){
239                 assert false;
240                 return null;
241             }
242             result.append(seq, capEnd, matchEnd);
243
244             pos = matchEnd;
245         }
246
247         if(pos == 0) return seq;
248
249         result.append(seq, pos, seq.length());
250
251         return result;
252     }
253
254     /**
255      * 任意の文字列をWiki表記へ変換する。
256      * @param seq 任意の文字列
257      * @return Wiki用表記
258      */
259     public static CharSequence escapeWikiSyntax(CharSequence seq){
260         CharSequence result = seq;
261         result = escapeWikiChar(result);   // この順番は大事
262         result = escapeWikiName(result);
263         // TODO さらにURLとメールアドレスのエスケープも
264         return result;
265     }
266
267     /**
268      * ブラケットに入れる文字をエスケープする。
269      * @param seq 文字列。
270      * @return エスケープされた文字列
271      */
272     public static CharSequence escapeWikiBracket(CharSequence seq){
273         StringBuilder result = new StringBuilder();
274
275         int seqLength = seq.length();
276         for(int pos = 0; pos < seqLength; pos++){
277             char ch = seq.charAt(pos);
278
279             switch(ch){
280             case '#': ch = '#'; break;
281             case '&': ch = '&'; break;
282             case '[': ch = '['; break;
283             case ']': ch = ']'; break;
284             case '<': ch = '<'; break;
285             case '>': ch = '>'; break;
286             default: break;
287             }
288
289             result.append(ch);
290         }
291
292         int resultLength;
293
294         while(result.length() > 0 && result.charAt(0) == '/'){
295             result.deleteCharAt(0);
296         }
297
298         resultLength = result.length();
299         for(int pos = resultLength - 1; pos >= 0; pos--){
300             char ch = result.charAt(pos);
301             if(ch != '/') break;
302             result.deleteCharAt(pos);
303         }
304
305         resultLength = result.length();
306         for(int pos = 1; pos < resultLength - 1; pos++){
307             char ch = result.charAt(pos);
308             if(ch == ':'){
309                 result.setCharAt(pos, ':');
310             }
311         }
312
313         resultLength = result.length();
314         if(resultLength == 1 && result.charAt(0) == ':'){
315             result.setCharAt(0, ':');
316         }
317
318         return result;
319     }
320
321     /**
322      * 数値参照文字に変換された文字を追加する。
323      * 例)'D' => "&#x44;"
324      * @param app 追加対象
325      * @param ch 1文字
326      * @return 引数と同じ
327      * @throws java.io.IOException 入出力エラー。文字列の場合はありえない。
328      */
329     public static Appendable appendNumCharRef(Appendable app, char ch)
330             throws IOException{
331         app.append("&#x");
332
333         int ival = ch;
334         String hex = Integer.toHexString(ival);
335         app.append(hex);
336
337         app.append(';');
338
339         return app;
340     }
341
342     /**
343      * 任意の文字を数値参照文字列に変換する。
344      * 例)'D' => "&#x44;"
345      * @param ch 文字
346      * @return 変換後の文字列
347      */
348     public static CharSequence toNumCharRef(char ch){
349         StringBuilder result = new StringBuilder(8);
350         try{
351             appendNumCharRef(result, ch);
352         }catch(IOException e){
353             assert false;
354             return null;
355         }
356         return result;
357     }
358
359     /**
360      * 陣営の色Wiki表記を返す。
361      * @param role 役職
362      * @return 色Wiki表記
363      */
364     public static String getTeamWikiColor(GameRole role){
365         String result;
366
367         switch(role){
368         case INNOCENT:
369         case SEER:
370         case SHAMAN:
371         case HUNTER:
372         case FRATER:
373             result = "#b7bad3";
374             break;
375         case WOLF:
376         case MADMAN:
377             result = "#e0b8b8";
378             break;
379         case HAMSTER:
380             result = "#b9d0be";
381             break;
382         default:
383             assert false;
384             return null;
385         }
386
387         return result;
388     }
389
390     /**
391      * 各役職のアイコンWikiを返す。
392      * @param role 役職
393      * @return アイコンWiki
394      */
395     public static String getRoleIconWiki(GameRole role){
396         String result;
397
398         switch(role){
399         case INNOCENT:
400             result = "&char(村人,nolink);";
401             break;
402         case WOLF:
403             result = "&char(人狼,nolink);";
404             break;
405         case SEER:
406             result = "&char(占い師,nolink);";
407             break;
408         case SHAMAN:
409             result = "&char(霊能者,nolink);";
410             break;
411         case MADMAN:
412             result = "&char(狂人,nolink);";
413             break;
414         case HUNTER:
415             result = "&char(狩人,nolink);";
416             break;
417         case FRATER:
418             result = "&char(共有者,nolink);";
419             break;
420         case HAMSTER:
421             result = "&char(ハムスター人間,nolink);";
422             break;
423         default:
424             assert false;
425             result = "";
426             break;
427         }
428
429         return result;
430     }
431
432     /**
433      * 運命に対応する色Wiki表記を返す。
434      * @param destiny 運命
435      * @return 色Wiki表記
436      */
437     public static String getDestinyColorWiki(Destiny destiny){
438         String result;
439         if(destiny == Destiny.ALIVE) result = "#ffffff";
440         else                         result = "#aaaaaa";
441         return result;
442     }
443
444     /**
445      * そのまままとめサイトパス名に使えそうなシンプルな文字か判定する。
446      * @param ch 文字
447      * @return まとめサイトパス名に使えそうならtrue
448      */
449     private static boolean isSimpleIdToken(char ch){
450         if('0' <= ch && ch <= '9') return true;
451         if('A' <= ch && ch <= 'Z') return true;
452         if('a' <= ch && ch <= 'z') return true;
453         if(ch == '-' || ch == '_') return true;
454         return false;
455     }
456
457     /**
458      * プレイヤーIDを構成する文字からパス名を組み立てる。
459      * @param seq パス名
460      * @param ch 文字
461      * @return 引数と同じもの
462      */
463     private static StringBuilder encodeId(StringBuilder seq, char ch){
464         if(isSimpleIdToken(ch)){
465             seq.append(ch);
466             return seq;
467         }
468
469         CharBuffer cbuf = CharBuffer.allocate(1);
470         cbuf.append(ch);
471         cbuf.rewind();
472
473         CharsetEncoder encoder = CHARSET_EUC.newEncoder();
474         ByteBuffer bytebuf;
475         try{
476             bytebuf = encoder.encode(cbuf);
477         }catch(CharacterCodingException e){
478             seq.append('X');
479             return seq;
480         }
481
482         int limit = bytebuf.limit();
483         while(bytebuf.position() < limit){
484             int iVal = bytebuf.get();
485             if(iVal < 0) iVal += 0x0100;
486             String hex = Integer.toHexString(iVal).toUpperCase(Locale.JAPAN);
487             seq.append('%');
488             if(hex.length() < 2) seq.append('0');
489             seq.append(hex);
490         }
491
492         return seq;
493     }
494
495     /**
496      * プレイヤーIDからパス名の一部を予測する。
497      * @param id プレイヤーID
498      * @return .htmlを抜いたパス名
499      */
500     private static StringBuilder encodeId(CharSequence id){
501         StringBuilder result = new StringBuilder();
502         int length = id.length();
503         for(int pt = 0; pt < length; pt++){
504             char ch = id.charAt(pt);
505             encodeId(result, ch);
506         }
507         return result;
508     }
509
510     /**
511      * プレイヤーIDからまとめサイト上の個人ページを推測する。
512      * @param id プレイヤーID
513      * @return 個人ページURL文字列
514      */
515     public static String encodeURLFromId(CharSequence id){
516         CharSequence encodedId = encodeId(id);
517
518         String result = WOLFBBS_URL + encodedId + ".html";
519
520         return result;
521     }
522
523 }