OSDN Git Service

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