LandDef.java

/*
 * land information definition
 *
 * License : The MIT License
 * Copyright(c) 2009 olyutorskii
 */

package jp.sourceforge.jindolf.corelib;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilder;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * 人狼BBSの各国設定。
 */
public final class LandDef{

    /** 各種イメージの相対なベースURI。 */
    public static final String IMAGE_RELPATH = "./plugin_wolf/img/";

    /** 顔アイコンURIのテンプレート。 */
    public static final String DEF_FACE_URI_TMPL =
        IMAGE_RELPATH + "face{0,number,#00}.jpg";
    /** デカキャラURIのテンプレート。 */
    public static final String DEF_BODY_URI_TMPL =
        IMAGE_RELPATH + "body{0,number,#00}.jpg";

    /** 墓小アイコンのデフォルト相対URI。 */
    public static final URI DEF_TOMBFACE_URI =
            URI.create(IMAGE_RELPATH + "face99.jpg").normalize();
    /** 墓大アイコンのデフォルト相対URI。 */
    public static final URI DEF_TOMBBODY_URI =
            URI.create(IMAGE_RELPATH + "body99.jpg").normalize();

    private static final Map<String, LandState> STATE_MAP;

    /** space or tab. */
    private static final String REG_POSIXBLANK = "\\p{Blank}";

    private static final char HYPHEN_CH = '-';
    private static final String HYPHEN = "-";
    private static final String COMMA = ",";


    static{
        STATE_MAP = new HashMap<>();
        STATE_MAP.put("closed",     LandState.CLOSED);
        STATE_MAP.put("historical", LandState.HISTORICAL);
        STATE_MAP.put("active",     LandState.ACTIVE);
    }


    private String landName;
    private String landId;
    private String formalName;
    private String landPrefix;
    private LandState landState;
    private int minMembers;
    private int maxMembers;
    private URI webURI;
    private URI cgiURI;
    private URI tombFaceIconURI;
    private URI tombBodyIconURI;
    private String faceURITemplate;
    private String bodyURITemplate;
    private Locale locale;
    private Charset encoding;
    private TimeZone timeZone;
    private long startDateTime;
    private long endDateTime;
    private String description;
    private String contactInfo;
    private int[] invalidVid;


    /**
     * コンストラクタ。
     */
    private LandDef(){
        super();
        return;
    }


    /**
     * ハイフンで区切られた整数範囲をパースする。
     * 「1-3」なら1,2,3を結果に格納する。
     * @param intSet 格納先Set
     * @param seq パース対象
     * @throws IllegalArgumentException 形式が変
     */
    private static void parseIntPair(Set<Integer> intSet, CharSequence seq)
            throws IllegalArgumentException{
        String token = seq.toString();

        String[] ivalues = token.split(HYPHEN);
        assert ivalues.length >= 1;
        if(ivalues.length >= 3){
            throw new IllegalArgumentException(token);
        }

        int ivalStart;
        int ivalEnd;
        try{
            ivalStart = Integer.parseInt(ivalues[0]);
            if(ivalues.length >= 2) ivalEnd = Integer.parseInt(ivalues[1]);
            else                    ivalEnd = ivalStart;
        }catch(NumberFormatException e){
            throw new IllegalArgumentException(token, e);
        }

        if(ivalStart > ivalEnd){
            int dummy = ivalStart;
            ivalStart = ivalEnd;
            ivalEnd = dummy;
            assert ivalStart <= ivalEnd;
        }

        for(int ival = ivalStart; ival <= ivalEnd; ival++){
            intSet.add(ival);
        }

        return;
    }

    /**
     * コンマとハイフンで区切られた整数の羅列をパースする。
     * 「10,23-25」なら10,23,24,25を結果に返す。
     * @param seq パース対象文字列
     * @return ソートされたIntegerのList
     * @throws IllegalArgumentException 形式が変。
     */
    public static SortedSet<Integer> parseIntList(CharSequence seq)
            throws IllegalArgumentException{
        SortedSet<Integer> result = new TreeSet<>();

        if(seq.length() <= 0 ) return result;
        String str = seq.toString();
        str = str.replaceAll(REG_POSIXBLANK, "");

        String[] tokens = str.split(COMMA);
        assert tokens.length >= 1;
        for(String token : tokens){
            if(token.length() <= 0) continue;
            if(token.charAt(0) == HYPHEN_CH || token.endsWith(HYPHEN)){
                throw new IllegalArgumentException(token);
            }
            parseIntPair(result, token);
        }

        return result;
    }

    /**
     * 国設定のListを返す。
     * @param builder DOMビルダ
     * @return List 国設定リスト
     * @throws IOException IOエラー
     * @throws SAXException パースエラー
     */
    public static List<LandDef> buildLandDefList(DocumentBuilder builder)
            throws IOException,
                   SAXException{
        List<Element> elemList = DomUtils.loadElemList(
                builder, XmlResource.I_URL_LANDDEF, "landDef");

        List<LandDef> result = new ArrayList<>(elemList.size());

        for(Element elem : elemList){
            LandDef landDef = buildLandDef(elem);
            result.add(landDef);
        }

        result = Collections.unmodifiableList(result);

        return result;
    }

    /**
     * ハイフンをデリミタに持つロケール指定文字列からLocaleを生成する。
     * @param attrVal ロケール指定文字列
     * @return Locale
     */
    public static Locale buildLocale(CharSequence attrVal){
        String tag = attrVal.toString();
        Locale locale = Locale.forLanguageTag(tag);
        return locale;
    }

    /**
     * XML属性を使って国定義の識別子情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillIdInfo(LandDef result, Element elem)
            throws SAXException{
        String landName   = DomUtils.attrRequired(elem, "landName");
        String landId     = DomUtils.attrRequired(elem, "landId");
        String formalName = DomUtils.attrRequired(elem, "formalName");
        String landPrefix = DomUtils.attrRequired(elem, "landPrefix");

        if(    landName  .length() <= 0
            || landId    .length() <= 0
            || formalName.length() <= 0 ){
            throw new SAXException("no identification info");
        }

        result.landName   = landName;
        result.landId     = landId;
        result.formalName = formalName;
        result.landPrefix = landPrefix;

        return;
    }

    /**
     * XML属性を使って国定義の定員情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillMemberInfo(LandDef result, Element elem)
            throws SAXException{
        String minStr = DomUtils.attrRequired(elem, "minMembers");
        String maxStr = DomUtils.attrRequired(elem, "maxMembers");

        int minMembers = Integer.parseInt(minStr);
        int maxMembers = Integer.parseInt(maxStr);

        if(    minMembers <= 0
            || minMembers > maxMembers ){
            throw new SAXException("invalid member limitation");
        }

        result.minMembers = minMembers;
        result.maxMembers = maxMembers;

        return;
    }

    /**
     * XML属性を使って国定義のURI情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillUriInfo(LandDef result, Element elem)
            throws SAXException{
        URI webURI = DomUtils.attrToUri(elem, "webURI");
        URI cgiURI = DomUtils.attrToUri(elem, "cgiURI");
        if(webURI == null || cgiURI == null){
            throw new SAXException("no URI");
        }
        if(    ! webURI.isAbsolute()
            || ! cgiURI.isAbsolute() ){
            throw new SAXException("relative URI");
        }

        URI tombFaceIconURI = DomUtils.attrToUri(elem, "tombFaceIconURI");
        URI tombBodyIconURI = DomUtils.attrToUri(elem, "tombBodyIconURI");
        if(tombFaceIconURI == null) tombFaceIconURI = DEF_TOMBFACE_URI;
        if(tombBodyIconURI == null) tombBodyIconURI = DEF_TOMBBODY_URI;

        result.webURI          = webURI;
        result.cgiURI          = cgiURI;
        result.tombFaceIconURI = tombFaceIconURI;
        result.tombBodyIconURI = tombBodyIconURI;

        return;
    }

    /**
     * XML属性を使って国定義のURIテンプレート情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillTemplateInfo(LandDef result, Element elem)
            throws SAXException{
        String faceURITemplate;
        String bodyURITemplate;

        faceURITemplate = DomUtils.attrValue(elem, "faceIconURITemplate");
        bodyURITemplate = DomUtils.attrValue(elem, "bodyIconURITemplate");

        if(faceURITemplate == null) faceURITemplate = DEF_FACE_URI_TMPL;
        if(bodyURITemplate == null) bodyURITemplate = DEF_BODY_URI_TMPL;

        result.faceURITemplate = faceURITemplate;
        result.bodyURITemplate = bodyURITemplate;

        return;
    }

    /**
     * XML属性を使って国定義の国際化情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillI18NInfo(LandDef result, Element elem)
            throws SAXException{
        String localeText   = DomUtils.attrRequired(elem, "locale");
        String encodingText = DomUtils.attrRequired(elem, "encoding");
        String timeZoneText = DomUtils.attrRequired(elem, "timeZone");

        Locale locale = buildLocale(localeText);
        Charset encoding = Charset.forName(encodingText);
        TimeZone timeZone = TimeZone.getTimeZone(timeZoneText);

        result.locale   = locale;
        result.encoding = encoding;
        result.timeZone = timeZone;

        return;
    }

    /**
     * XML属性を使って国定義の日付情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillDateInfo(LandDef result, Element elem)
            throws SAXException{
        long startDateTime;
        long endDateTime;

        String startDateText = DomUtils.attrRequired(elem, "startDate");
        String endDateText = elem.getAttribute("endDate");

        startDateTime = DateUtils.parseISO8601(startDateText);

        if(endDateText.length() > 0){
            endDateTime = DateUtils.parseISO8601(endDateText);
        }else{
            endDateTime = -1;
        }

        if(startDateTime < 0){
            throw new SAXException("illegal start date " + startDateText);
        }

        if(endDateTime >= 0 && startDateTime > endDateTime){
            throw new SAXException("start date is too old " + startDateText);
        }

        result.startDateTime = startDateTime;
        result.endDateTime   = endDateTime;

        return;
    }

    /**
     * XML属性を使って国定義の各種ステータス情報を埋める。
     * @param result 国定義
     * @param elem 個別のXML国定義要素
     * @throws SAXException XML属性の記述に関する異常系
     */
    private static void fillLandInfo(LandDef result, Element elem)
            throws SAXException{
        String state = DomUtils.attrRequired(elem, "landState");
        LandState landState = STATE_MAP.get(state);
        if(landState == null){
            throw new SAXException("illegal land status " + state);
        }

        String description = DomUtils.attrRequired(elem, "description");
        String contactInfo = DomUtils.attrRequired(elem, "contactInfo");

        String invalidVid = elem.getAttribute("invalidVid");
        SortedSet<Integer> invalidSet = parseIntList(invalidVid);
        int[] invalidArray = new int[invalidSet.size()];
        int pos = 0;
        for(int vid : invalidSet){
            invalidArray[pos++] = vid;
        }

        result.landState   = landState;
        result.description = description;
        result.contactInfo = contactInfo;
        result.invalidVid  = invalidArray;

        return;
    }

    /**
     * 個々の国設定をオブジェクトに変換する。
     * @param elem 国設定要素
     * @return 国設定オブジェクト
     * @throws SAXException パースエラー
     */
    private static LandDef buildLandDef(Element elem)
            throws SAXException{
        LandDef result = new LandDef();

        fillLandInfo    (result, elem);
        fillIdInfo      (result, elem);
        fillMemberInfo  (result, elem);
        fillUriInfo     (result, elem);
        fillTemplateInfo(result, elem);
        fillI18NInfo    (result, elem);
        fillDateInfo    (result, elem);

        return result;
    }


    /**
     * 国名を得る。
     * @return 国名
     */
    public String getLandName(){
        return this.landName;
    }

    /**
     * 国識別子を得る。
     * @return 識別子
     */
    public String getLandId(){
        return this.landId;
    }

    /**
     * 正式名称を得る。
     * @return 正式名称
     */
    public String getFormalName(){
        return this.formalName;
    }

    /**
     * 各村の前置文字。
     * F国なら「F」
     * @return 前置文字
     */
    public String getLandPrefix(){
        return this.landPrefix;
    }

    /**
     * 国の状態を得る。
     * @return 状態
     */
    public LandState getLandState(){
        return this.landState;
    }

    /**
     * 最小定員を得る。
     * @return 最小定員
     */
    public int getMinMembers(){
        return this.minMembers;
    }

    /**
     * 最大定員を得る。
     * @return 最大定員
     */
    public int getMaxMembers(){
        return this.maxMembers;
    }

    /**
     * Webアクセス用の入り口URIを得る。
     * @return 入り口URI
     */
    public URI getWebURI(){
        return this.webURI;
    }

    /**
     * クエリーを投げるCGIのURIを得る。
     * @return CGIのURI
     */
    public URI getCgiURI(){
        return this.cgiURI;
    }

    /**
     * 墓画像のURIを得る。
     * @return 墓URI
     */
    public URI getTombFaceIconURI(){
        return this.tombFaceIconURI;
    }

    /**
     * 大きな墓画像のURIを得る。
     * @return 墓URI
     */
    public URI getTombBodyIconURI(){
        return this.tombBodyIconURI;
    }

    /**
     * 顔アイコンURIのテンプレートを得る。
     * @return Formatter用テンプレート
     */
    public String getFaceURITemplate(){
        return this.faceURITemplate;
    }

    /**
     * 全身像アイコンURIのテンプレートを得る。
     * @return Formatter用テンプレート
     */
    public String getBodyURITemplate(){
        return this.bodyURITemplate;
    }

    /**
     * この国のロケールを得る。
     * @return ロケール
     */
    public Locale getLocale(){
        return this.locale;
    }

    /**
     * この国が使うエンコーディングを得る。
     * @return エンコーディング
     */
    public Charset getEncoding(){
        return this.encoding;
    }

    /**
     * この国の時刻表記で使うタイムゾーンのコピーを得る。
     * @return タイムゾーン
     */
    public TimeZone getTimeZone(){
        Object copy = this.timeZone.clone();
        TimeZone result = (TimeZone) copy;
        return result;
    }

    /**
     * この国の始まった時刻を得る。
     * @return 始まった時刻(エポックミリ秒)。
     */
    public long getStartDateTime(){
        return this.startDateTime;
    }

    /**
     * この国が発言を打ち切った時刻を得る。
     * @return 打ち切った時刻(エポックミリ秒)。まだ打ち切っていない場合は負。
     */
    public long getEndDateTime(){
        return this.endDateTime;
    }

    /**
     * この国の説明を得る。
     * @return 説明文字列
     */
    public String getDescription(){
        return this.description;
    }

    /**
     * この国の連絡先を得る。
     * @return 連絡先文字列
     */
    public String getContactInfo(){
        return this.contactInfo;
    }

    /**
     * 有効な村IDか否か判定する。
     * @param vid 村ID
     * @return 無効な村ならfalse
     */
    public boolean isValidVillageId(int vid){
        int pos = Arrays.binarySearch(this.invalidVid, vid);
        if(pos >= 0) return false;
        return true;
    }

}