View Javadoc
1   /*
2    * land information definition
3    *
4    * License : The MIT License
5    * Copyright(c) 2009 olyutorskii
6    */
7   
8   package jp.sourceforge.jindolf.corelib;
9   
10  import java.io.IOException;
11  import java.net.URI;
12  import java.nio.charset.Charset;
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.Collections;
16  import java.util.HashMap;
17  import java.util.List;
18  import java.util.Locale;
19  import java.util.Map;
20  import java.util.Set;
21  import java.util.SortedSet;
22  import java.util.TimeZone;
23  import java.util.TreeSet;
24  import javax.xml.parsers.DocumentBuilder;
25  import org.w3c.dom.Element;
26  import org.xml.sax.SAXException;
27  
28  /**
29   * 人狼BBSの各国設定。
30   */
31  public final class LandDef{
32  
33      /** 各種イメージの相対なベースURI。 */
34      public static final String IMAGE_RELPATH = "./plugin_wolf/img/";
35  
36      /** 顔アイコンURIのテンプレート。 */
37      public static final String DEF_FACE_URI_TMPL =
38          IMAGE_RELPATH + "face{0,number,#00}.jpg";
39      /** デカキャラURIのテンプレート。 */
40      public static final String DEF_BODY_URI_TMPL =
41          IMAGE_RELPATH + "body{0,number,#00}.jpg";
42  
43      /** 墓小アイコンのデフォルト相対URI。 */
44      public static final URI DEF_TOMBFACE_URI =
45              URI.create(IMAGE_RELPATH + "face99.jpg").normalize();
46      /** 墓大アイコンのデフォルト相対URI。 */
47      public static final URI DEF_TOMBBODY_URI =
48              URI.create(IMAGE_RELPATH + "body99.jpg").normalize();
49  
50      private static final Map<String, LandState> STATE_MAP;
51  
52      /** space or tab. */
53      private static final String REG_POSIXBLANK = "\\p{Blank}";
54  
55      private static final char HYPHEN_CH = '-';
56      private static final String HYPHEN = "-";
57      private static final String COMMA = ",";
58  
59  
60      static{
61          STATE_MAP = new HashMap<>();
62          STATE_MAP.put("closed",     LandState.CLOSED);
63          STATE_MAP.put("historical", LandState.HISTORICAL);
64          STATE_MAP.put("active",     LandState.ACTIVE);
65      }
66  
67  
68      private String landName;
69      private String landId;
70      private String formalName;
71      private String landPrefix;
72      private LandState landState;
73      private int minMembers;
74      private int maxMembers;
75      private URI webURI;
76      private URI cgiURI;
77      private URI tombFaceIconURI;
78      private URI tombBodyIconURI;
79      private String faceURITemplate;
80      private String bodyURITemplate;
81      private Locale locale;
82      private Charset encoding;
83      private TimeZone timeZone;
84      private long startDateTime;
85      private long endDateTime;
86      private String description;
87      private String contactInfo;
88      private int[] invalidVid;
89  
90  
91      /**
92       * コンストラクタ。
93       */
94      private LandDef(){
95          super();
96          return;
97      }
98  
99  
100     /**
101      * ハイフンで区切られた整数範囲をパースする。
102      * 「1-3」なら1,2,3を結果に格納する。
103      * @param intSet 格納先Set
104      * @param seq パース対象
105      * @throws IllegalArgumentException 形式が変
106      */
107     private static void parseIntPair(Set<Integer> intSet, CharSequence seq)
108             throws IllegalArgumentException{
109         String token = seq.toString();
110 
111         String[] ivalues = token.split(HYPHEN);
112         assert ivalues.length >= 1;
113         if(ivalues.length >= 3){
114             throw new IllegalArgumentException(token);
115         }
116 
117         int ivalStart;
118         int ivalEnd;
119         try{
120             ivalStart = Integer.parseInt(ivalues[0]);
121             if(ivalues.length >= 2) ivalEnd = Integer.parseInt(ivalues[1]);
122             else                    ivalEnd = ivalStart;
123         }catch(NumberFormatException e){
124             throw new IllegalArgumentException(token, e);
125         }
126 
127         if(ivalStart > ivalEnd){
128             int dummy = ivalStart;
129             ivalStart = ivalEnd;
130             ivalEnd = dummy;
131             assert ivalStart <= ivalEnd;
132         }
133 
134         for(int ival = ivalStart; ival <= ivalEnd; ival++){
135             intSet.add(ival);
136         }
137 
138         return;
139     }
140 
141     /**
142      * コンマとハイフンで区切られた整数の羅列をパースする。
143      * 「10,23-25」なら10,23,24,25を結果に返す。
144      * @param seq パース対象文字列
145      * @return ソートされたIntegerのList
146      * @throws IllegalArgumentException 形式が変。
147      */
148     public static SortedSet<Integer> parseIntList(CharSequence seq)
149             throws IllegalArgumentException{
150         SortedSet<Integer> result = new TreeSet<>();
151 
152         if(seq.length() <= 0 ) return result;
153         String str = seq.toString();
154         str = str.replaceAll(REG_POSIXBLANK, "");
155 
156         String[] tokens = str.split(COMMA);
157         assert tokens.length >= 1;
158         for(String token : tokens){
159             if(token.length() <= 0) continue;
160             if(token.charAt(0) == HYPHEN_CH || token.endsWith(HYPHEN)){
161                 throw new IllegalArgumentException(token);
162             }
163             parseIntPair(result, token);
164         }
165 
166         return result;
167     }
168 
169     /**
170      * 国設定のListを返す。
171      * @param builder DOMビルダ
172      * @return List 国設定リスト
173      * @throws IOException IOエラー
174      * @throws SAXException パースエラー
175      */
176     public static List<LandDef> buildLandDefList(DocumentBuilder builder)
177             throws IOException,
178                    SAXException{
179         List<Element> elemList = DomUtils.loadElemList(
180                 builder, XmlResource.I_URL_LANDDEF, "landDef");
181 
182         List<LandDef> result = new ArrayList<>(elemList.size());
183 
184         for(Element elem : elemList){
185             LandDef landDef = buildLandDef(elem);
186             result.add(landDef);
187         }
188 
189         result = Collections.unmodifiableList(result);
190 
191         return result;
192     }
193 
194     /**
195      * ハイフンをデリミタに持つロケール指定文字列からLocaleを生成する。
196      * @param attrVal ロケール指定文字列
197      * @return Locale
198      */
199     public static Locale buildLocale(CharSequence attrVal){
200         String tag = attrVal.toString();
201         Locale locale = Locale.forLanguageTag(tag);
202         return locale;
203     }
204 
205     /**
206      * XML属性を使って国定義の識別子情報を埋める。
207      * @param result 国定義
208      * @param elem 個別のXML国定義要素
209      * @throws SAXException XML属性の記述に関する異常系
210      */
211     private static void fillIdInfo(LandDef result, Element elem)
212             throws SAXException{
213         String landName   = DomUtils.attrRequired(elem, "landName");
214         String landId     = DomUtils.attrRequired(elem, "landId");
215         String formalName = DomUtils.attrRequired(elem, "formalName");
216         String landPrefix = DomUtils.attrRequired(elem, "landPrefix");
217 
218         if(    landName  .length() <= 0
219             || landId    .length() <= 0
220             || formalName.length() <= 0 ){
221             throw new SAXException("no identification info");
222         }
223 
224         result.landName   = landName;
225         result.landId     = landId;
226         result.formalName = formalName;
227         result.landPrefix = landPrefix;
228 
229         return;
230     }
231 
232     /**
233      * XML属性を使って国定義の定員情報を埋める。
234      * @param result 国定義
235      * @param elem 個別のXML国定義要素
236      * @throws SAXException XML属性の記述に関する異常系
237      */
238     private static void fillMemberInfo(LandDef result, Element elem)
239             throws SAXException{
240         String minStr = DomUtils.attrRequired(elem, "minMembers");
241         String maxStr = DomUtils.attrRequired(elem, "maxMembers");
242 
243         int minMembers = Integer.parseInt(minStr);
244         int maxMembers = Integer.parseInt(maxStr);
245 
246         if(    minMembers <= 0
247             || minMembers > maxMembers ){
248             throw new SAXException("invalid member limitation");
249         }
250 
251         result.minMembers = minMembers;
252         result.maxMembers = maxMembers;
253 
254         return;
255     }
256 
257     /**
258      * XML属性を使って国定義のURI情報を埋める。
259      * @param result 国定義
260      * @param elem 個別のXML国定義要素
261      * @throws SAXException XML属性の記述に関する異常系
262      */
263     private static void fillUriInfo(LandDef result, Element elem)
264             throws SAXException{
265         URI webURI = DomUtils.attrToUri(elem, "webURI");
266         URI cgiURI = DomUtils.attrToUri(elem, "cgiURI");
267         if(webURI == null || cgiURI == null){
268             throw new SAXException("no URI");
269         }
270         if(    ! webURI.isAbsolute()
271             || ! cgiURI.isAbsolute() ){
272             throw new SAXException("relative URI");
273         }
274 
275         URI tombFaceIconURI = DomUtils.attrToUri(elem, "tombFaceIconURI");
276         URI tombBodyIconURI = DomUtils.attrToUri(elem, "tombBodyIconURI");
277         if(tombFaceIconURI == null) tombFaceIconURI = DEF_TOMBFACE_URI;
278         if(tombBodyIconURI == null) tombBodyIconURI = DEF_TOMBBODY_URI;
279 
280         result.webURI          = webURI;
281         result.cgiURI          = cgiURI;
282         result.tombFaceIconURI = tombFaceIconURI;
283         result.tombBodyIconURI = tombBodyIconURI;
284 
285         return;
286     }
287 
288     /**
289      * XML属性を使って国定義のURIテンプレート情報を埋める。
290      * @param result 国定義
291      * @param elem 個別のXML国定義要素
292      * @throws SAXException XML属性の記述に関する異常系
293      */
294     private static void fillTemplateInfo(LandDef result, Element elem)
295             throws SAXException{
296         String faceURITemplate;
297         String bodyURITemplate;
298 
299         faceURITemplate = DomUtils.attrValue(elem, "faceIconURITemplate");
300         bodyURITemplate = DomUtils.attrValue(elem, "bodyIconURITemplate");
301 
302         if(faceURITemplate == null) faceURITemplate = DEF_FACE_URI_TMPL;
303         if(bodyURITemplate == null) bodyURITemplate = DEF_BODY_URI_TMPL;
304 
305         result.faceURITemplate = faceURITemplate;
306         result.bodyURITemplate = bodyURITemplate;
307 
308         return;
309     }
310 
311     /**
312      * XML属性を使って国定義の国際化情報を埋める。
313      * @param result 国定義
314      * @param elem 個別のXML国定義要素
315      * @throws SAXException XML属性の記述に関する異常系
316      */
317     private static void fillI18NInfo(LandDef result, Element elem)
318             throws SAXException{
319         String localeText   = DomUtils.attrRequired(elem, "locale");
320         String encodingText = DomUtils.attrRequired(elem, "encoding");
321         String timeZoneText = DomUtils.attrRequired(elem, "timeZone");
322 
323         Locale locale = buildLocale(localeText);
324         Charset encoding = Charset.forName(encodingText);
325         TimeZone timeZone = TimeZone.getTimeZone(timeZoneText);
326 
327         result.locale   = locale;
328         result.encoding = encoding;
329         result.timeZone = timeZone;
330 
331         return;
332     }
333 
334     /**
335      * XML属性を使って国定義の日付情報を埋める。
336      * @param result 国定義
337      * @param elem 個別のXML国定義要素
338      * @throws SAXException XML属性の記述に関する異常系
339      */
340     private static void fillDateInfo(LandDef result, Element elem)
341             throws SAXException{
342         long startDateTime;
343         long endDateTime;
344 
345         String startDateText = DomUtils.attrRequired(elem, "startDate");
346         String endDateText = elem.getAttribute("endDate");
347 
348         startDateTime = DateUtils.parseISO8601(startDateText);
349 
350         if(endDateText.length() > 0){
351             endDateTime = DateUtils.parseISO8601(endDateText);
352         }else{
353             endDateTime = -1;
354         }
355 
356         if(startDateTime < 0){
357             throw new SAXException("illegal start date " + startDateText);
358         }
359 
360         if(endDateTime >= 0 && startDateTime > endDateTime){
361             throw new SAXException("start date is too old " + startDateText);
362         }
363 
364         result.startDateTime = startDateTime;
365         result.endDateTime   = endDateTime;
366 
367         return;
368     }
369 
370     /**
371      * XML属性を使って国定義の各種ステータス情報を埋める。
372      * @param result 国定義
373      * @param elem 個別のXML国定義要素
374      * @throws SAXException XML属性の記述に関する異常系
375      */
376     private static void fillLandInfo(LandDef result, Element elem)
377             throws SAXException{
378         String state = DomUtils.attrRequired(elem, "landState");
379         LandState landState = STATE_MAP.get(state);
380         if(landState == null){
381             throw new SAXException("illegal land status " + state);
382         }
383 
384         String description = DomUtils.attrRequired(elem, "description");
385         String contactInfo = DomUtils.attrRequired(elem, "contactInfo");
386 
387         String invalidVid = elem.getAttribute("invalidVid");
388         SortedSet<Integer> invalidSet = parseIntList(invalidVid);
389         int[] invalidArray = new int[invalidSet.size()];
390         int pos = 0;
391         for(int vid : invalidSet){
392             invalidArray[pos++] = vid;
393         }
394 
395         result.landState   = landState;
396         result.description = description;
397         result.contactInfo = contactInfo;
398         result.invalidVid  = invalidArray;
399 
400         return;
401     }
402 
403     /**
404      * 個々の国設定をオブジェクトに変換する。
405      * @param elem 国設定要素
406      * @return 国設定オブジェクト
407      * @throws SAXException パースエラー
408      */
409     private static LandDef buildLandDef(Element elem)
410             throws SAXException{
411         LandDef result = new LandDef();
412 
413         fillLandInfo    (result, elem);
414         fillIdInfo      (result, elem);
415         fillMemberInfo  (result, elem);
416         fillUriInfo     (result, elem);
417         fillTemplateInfo(result, elem);
418         fillI18NInfo    (result, elem);
419         fillDateInfo    (result, elem);
420 
421         return result;
422     }
423 
424 
425     /**
426      * 国名を得る。
427      * @return 国名
428      */
429     public String getLandName(){
430         return this.landName;
431     }
432 
433     /**
434      * 国識別子を得る。
435      * @return 識別子
436      */
437     public String getLandId(){
438         return this.landId;
439     }
440 
441     /**
442      * 正式名称を得る。
443      * @return 正式名称
444      */
445     public String getFormalName(){
446         return this.formalName;
447     }
448 
449     /**
450      * 各村の前置文字。
451      * F国なら「F」
452      * @return 前置文字
453      */
454     public String getLandPrefix(){
455         return this.landPrefix;
456     }
457 
458     /**
459      * 国の状態を得る。
460      * @return 状態
461      */
462     public LandState getLandState(){
463         return this.landState;
464     }
465 
466     /**
467      * 最小定員を得る。
468      * @return 最小定員
469      */
470     public int getMinMembers(){
471         return this.minMembers;
472     }
473 
474     /**
475      * 最大定員を得る。
476      * @return 最大定員
477      */
478     public int getMaxMembers(){
479         return this.maxMembers;
480     }
481 
482     /**
483      * Webアクセス用の入り口URIを得る。
484      * @return 入り口URI
485      */
486     public URI getWebURI(){
487         return this.webURI;
488     }
489 
490     /**
491      * クエリーを投げるCGIのURIを得る。
492      * @return CGIのURI
493      */
494     public URI getCgiURI(){
495         return this.cgiURI;
496     }
497 
498     /**
499      * 墓画像のURIを得る。
500      * @return 墓URI
501      */
502     public URI getTombFaceIconURI(){
503         return this.tombFaceIconURI;
504     }
505 
506     /**
507      * 大きな墓画像のURIを得る。
508      * @return 墓URI
509      */
510     public URI getTombBodyIconURI(){
511         return this.tombBodyIconURI;
512     }
513 
514     /**
515      * 顔アイコンURIのテンプレートを得る。
516      * @return Formatter用テンプレート
517      */
518     public String getFaceURITemplate(){
519         return this.faceURITemplate;
520     }
521 
522     /**
523      * 全身像アイコンURIのテンプレートを得る。
524      * @return Formatter用テンプレート
525      */
526     public String getBodyURITemplate(){
527         return this.bodyURITemplate;
528     }
529 
530     /**
531      * この国のロケールを得る。
532      * @return ロケール
533      */
534     public Locale getLocale(){
535         return this.locale;
536     }
537 
538     /**
539      * この国が使うエンコーディングを得る。
540      * @return エンコーディング
541      */
542     public Charset getEncoding(){
543         return this.encoding;
544     }
545 
546     /**
547      * この国の時刻表記で使うタイムゾーンのコピーを得る。
548      * @return タイムゾーン
549      */
550     public TimeZone getTimeZone(){
551         Object copy = this.timeZone.clone();
552         TimeZone result = (TimeZone) copy;
553         return result;
554     }
555 
556     /**
557      * この国の始まった時刻を得る。
558      * @return 始まった時刻(エポックミリ秒)。
559      */
560     public long getStartDateTime(){
561         return this.startDateTime;
562     }
563 
564     /**
565      * この国が発言を打ち切った時刻を得る。
566      * @return 打ち切った時刻(エポックミリ秒)。まだ打ち切っていない場合は負。
567      */
568     public long getEndDateTime(){
569         return this.endDateTime;
570     }
571 
572     /**
573      * この国の説明を得る。
574      * @return 説明文字列
575      */
576     public String getDescription(){
577         return this.description;
578     }
579 
580     /**
581      * この国の連絡先を得る。
582      * @return 連絡先文字列
583      */
584     public String getContactInfo(){
585         return this.contactInfo;
586     }
587 
588     /**
589      * 有効な村IDか否か判定する。
590      * @param vid 村ID
591      * @return 無効な村ならfalse
592      */
593     public boolean isValidVillageId(int vid){
594         int pos = Arrays.binarySearch(this.invalidVid, vid);
595         if(pos >= 0) return false;
596         return true;
597     }
598 
599 }