JSP/ServletでBBS作成メモ

とりあえず, あんまり前に進んでないBBS本体の作成はおいといて,
まえに作ってたHTMLタグ認識, ってのを完成に近づけることにした.



やったことは1つ.
httpとかhttpsではじまるURL文字列にリンクを貼るだけ.
メールアドレスも認識するようにすべきか!とか思ったけど, やめた.
そこは本分じゃない気がする.
同様にttpから初まるのもリンクすべきか?とか思ったけど,
書いた人は直リンしないようにそうしているんだから, 貼っちゃまずいだろってことでやめた.


正直, かなり大変だった.
まさか, Character.isLetterOrDigitが全角文字もちゃんと解釈するようにできてるとは.
めちゃくちゃとまどった.
一応, はてなみたいに, [ ]と[]でURLを囲ったらリンクしないようにした.
本当は, 借りてきた本みたいに, 正規表現使って一気に置換すれば楽なんだろうけど,
このリンク停止がそれじゃどうしても実装できなかったから, 一文字づつ処理して判断するようにした.



ちょっと量多いけど, うーん...,
まあ, いいや, ソースはっちゃえ.

/********************************************************************************
 * テキストエリアへの入力を変換する関数
 * - 特定のHTMLタグの入力を許可.
 * - 1行を<p> ... </p>タグでくくる.
 * ------------------------------------------------------------
 * TranslateInputData tr = new TranslateInputData( 入力文字列 );
 * // 入力文字列を変換する.
 * tr.translate();
 * // 変換後の文字列を取得する.
 * String output = tr.getTranslatedText;
 * // 変換前の文字列を取得する.
 * String input  = tr.getOriginalText;
 ********************************************************************************/

package test.textarea;

import java.util.*;
import java.util.regex.*;

public class TranslateInputData
{
    /**
     * 入力文字列
     */
    private String input;

    /**
     * 出力文字列
     */
    private StringBuffer output;

    /**
     * Pタグでくくるかどうかを判定するフィールド
     * == 0 : Pタグを出力
     * >= 1 : Pタグを出力しない
     */
    private int PTAG_OFF;

    /**
     * 今読み込んでるタグが開始タグか終端タグかどうかを示す変数
     * == 1 : 開始タグ
     * == 2 : 終端タグ
     */
    private int SLASH;

    /**
     * 引数なしのコンストラクタ
     * 一応, 作成しておいた.
     * 使用する場合, この後, setOriginalText() をする必要あり.
     */
    public TranslateInputData()
    {
        this.output = new StringBuffer();
        this.PTAG_OFF = 0;
        this.SLASH    = 1;
    }

    /**
     * 変換前の文字列を引数にとるコンストラクタ
     */
    public TranslateInputData( String input )
    {
        this.input  = input + '\n'; // 念のため, 文末に改行をつけておく.
        this.output = new StringBuffer();
        this.PTAG_OFF = 0;
        this.SLASH    = 1;
    }

    /**
     * 変換するテキストを設定する.
     */
    public void setOriginalText( String input )
    {
        // 念のため, 文末に改行をつけておく.
        this.input = input + '\n';
    }

    /**
     * 変換前のテキストを取得する.
     */
    public String getOriginalText()
    {
        return input;
    }

    /**
     * 変換後のテキストを取得する.
     */
    public String getTranslatedText()
    {
        //        return getAnchorToUrlTag( output.toString() );
        return output.toString();
    }

    /**
     * 'h'から初まる文字列がURL文字列かどうかを検査し, もしそうだったならば, それを変換した文字列を返す関数
     * なお, 検査するのは, httpとhttpsのみ.
     * ttpや, ftpは検査しない.
     * Java + MySQL + Tomcatで作る掲示板とブログのソースより流用.
     */
    public String getAnchorToUrlTag( String input )
    {
        String  urlPattern = "(http://|https://)[\\w\\[\\]\\-:/?#@!$&'()*+,;=.~%]+";
        Pattern    pattern = Pattern.compile( urlPattern, Pattern.CASE_INSENSITIVE );
        Matcher    matcher = pattern.matcher( input );

        return matcher.replaceAll( "<a href=$0>$0</a>" );
    }

    /**
     * 入力文字列をしかるべき形式に変換し, StrinbBuffer型変数に代入するメソッド.
     */
    public void translate()
    {
        char c;                 // 現在読み込んでいる文字
        int     count = 0;              // 入力文字列において読み込んだ文字数
        int    length = input.length(); // 入力文字列の長さ
        int taglength = 0;              // タグの長さ

        boolean DOCTOPLF  = false;

        // まずは文頭の処理
        if( length > 1 ){
            if( input.charAt( 1 ) == '\n' || input.charAt( 1 ) == '\r' ){
                // 次の文字が改行ならば, <br>.
                output.append( "<br>\n" );
                PTAG_OFF++;
                DOCTOPLF = true;
            } else {
                // 1行をpタグでくくる.
                output.append( "<p>" );
            }
        }

        for( count=0; count<length; ++count ){
            c = input.charAt(count);
            switch(c){
            case  '&':
                output.append( "&amp;" );
                break;
            case  '>':
                output.append( "&gt;" );
                break;
            case  '<':
                // 終端タグかどうかをチェックする.
                if( input.charAt( count+1 ) == '/' ){ SLASH = 2; }
                // SLASHは最低でも1.
                // '<', '</'の次の文字を指すようにするため.
                taglength = getTagLength( count+SLASH );
                if( taglength > 0 ){
                    // 第2引数で+1としているのは,
                    // substringが書き込む文字列の最終文字の次を指定しなければならないため.
                    output.append( input.substring( count, count+SLASH+taglength+1 ) );
                    // 読み込んだ文字数を更新
                    count += SLASH+taglength;
                } else {
                    output.append( "&lt;" );
                }
                taglength = 0;  // 初期状態に戻しておく
                SLASH     = 1;  // 必ずSLASH=1に戻す
                break;
            case  ' ':
                output.append( "&nbsp;");
                break;
            case '\"':
                output.append( "&quot;" );
                break;
            case '\'':
                output.append( "&#39;" );
                break;
            case '\t':
                // タグは空白4つ
                output.append( "&nbsp;&nbsp;&nbsp;&nbsp;" );
                break;
            case  'h':
                taglength = getAnchorTagFromUrl( count );
                if( taglength > 0 ){
                    // 汚ないIF文
                    // 解決法を思い付いたら直す事
                    if( count > 1 && input.substring( count-2, count ).equals( "[]" ) == true ){
                        if( input.substring( count+taglength-2, count+taglength ).equals( "[]" ) == false ){
                            // []url
                            output.append( "<a href=" +input.substring( count, count+taglength )+ ">" +input.substring( count, count+taglength )+ "</a>" );
                            count += taglength -1;
                        } else {
                            // []url[]
                            output.append( "h" );
                        }
                    } else if( input.substring( count+taglength-2, count+taglength ).equals( "[]" ) == false ){
                        // url
                        output.append( "<a href=" +input.substring( count, count+taglength )+ ">" +input.substring( count, count+taglength )+ "</a>" );
                        count += taglength -1;
                    } else {
                        // url[]
                        output.append( "<a href=" +input.substring( count, count+taglength-2 )+ ">" +input.substring( count, count+taglength-2 )+ "</a>" );
                        count += taglength -3;
                    }
                } else {
                    output.append( "h" );
                }
                taglength = 0;
                break;
            case '\r':
                // こいつは無視する. 大丈夫かなぁ?
                break;
            case '\n':
                if(  count+1 < length ){
                    // なんか文字コードEUCに指定しているのに, 改行は\r\nみたい.
                    // 上手い改行の検出方法ってないかなあ.
                    if( input.charAt( count+1 ) == '\n' || input.charAt( count+1 ) == '\r' ){
                        output.append( "<br>\n" );
                    } else if( PTAG_OFF == 0 ){
                        output.append( "</p>\n<p>" );
                    } else if( DOCTOPLF == false ){
                        output.append( "\n" );
                    } else if( DOCTOPLF == true ){
                        output.append( "<p>" );
                        DOCTOPLF = false;
                        PTAG_OFF--;
                    }
                } else if( count+1 == length ){
                    // 最後の1文字のための処理
                    if( PTAG_OFF == 0 ){
                        output.append( "</p>\n" );
                    } else {
                        output.append( "\n" );
                    }
                }
                break;
            default:
                output.append(c);
                break;
            }
        }
    }

    private int getAnchorTagFromUrl( int start )
    {
        char c;
        String url;
        int count = 1;
        int pref  = 0;

        for(;;count++){

            if( count == 9 ){ return 0; }
            c = input.charAt( start+count );
            if( c == '\n' || c == ' ' ){ return 0; }

            url = input.substring( start, start+count );
            if( url.equals( "http://" ) == true || url.equals( "https://" ) == true ){
                pref = count;
                break;
            }
        }

        for(;;count++){
            c = input.charAt( start+count );

            if( c >= '0' && c <= '9' ){ continue; }
            else if( c >= 'A' && c <= 'z' ){ continue; }
            else if( c ==  '[' ){ continue; }
            else if( c ==  ']' ){ continue; }
            else if( c ==  '-' ){ continue; }
            else if( c ==  ':' ){ continue; }
            else if( c ==  '/' ){ continue; }
            else if( c ==  '?' ){ continue; }
            else if( c ==  '#' ){ continue; }
            else if( c ==  '@' ){ continue; }
            else if( c ==  '!' ){ continue; }
            else if( c ==  '$' ){ continue; }
            else if( c ==  '&' ){ continue; }
            else if( c == '\'' ){ continue; }
            else if( c ==  '(' ){ continue; }
            else if( c ==  ')' ){ continue; }
            else if( c ==  '*' ){ continue; }
            else if( c ==  '+' ){ continue; }
            else if( c ==  ',' ){ continue; }
            else if( c ==  ';' ){ continue; }
            else if( c ==  '=' ){ continue; }
            else if( c ==  '.' ){ continue; }
            else if( c ==  '~' ){ continue; }
            else if( c ==  '%' ){ continue; }
            else { break; }
        }

        if( pref == count ){ count = 0; }
        return count;
    }

   /**
     * '<'("</")以降の文字列がタグかどうかを検査する関数
     * タグであれば0以上の値が, そうでなければ, 0が返戻される.
     */
    private int getTagLength( int start )
    {
        char c;   // 一時保存用
        int     check =  0;     // 検証した文字数
        int       tag = -1;     // タグならば, 0以上の値をとる

        // 最大の長さのタグ, BLOCKQUOTEが10のため, 10個先まで検証.
        for( int i=1; i<11; i++ ){

            // 次の文字が改行, また, 空白だったならば, 終了.
            c = input.charAt( start+i-1 );
            if( c == '\n' || c == ' ' ){ return 0; }

            try{
                // <BLOCKQUOTE .... だったならば, BLOCKQUOTEの部分と一致するかどうか.
                tag = map.get( input.substring( start, start+i ).toUpperCase() );
            } catch( NullPointerException e ){
                // Hashtableに登録されていなかった.
                tag = -1;
            }

            // タグの文字列の次の文字は, 空白か, '>'でなければならない.
            if( tag > 0 ){
                c = input.charAt( start+i );
                if( c == ' ' || c == '>' ){
                    check = i;
                    break;
                }
                tag = -1;
            } else if ( tag == 0 ){
                // コメントだったならば,
                check = i;
                break;
            }
        }

        // 各タグそれぞれにおける詳細なチェックを行う.
        // tag変数からタグ長を取れるようにすれば, checkは無くてもいいかもしれない.
        // でも, それを実装するより, checkを直に渡した方が楽.
        check = getResultOfTagCheck( tag, start, check );

        return check;
    }

    /**
     * タグ毎に詳細な処理をする関数に飛ばすメソッド
     */
    private int getResultOfTagCheck( int tag, int start, int check )
    {
        int chk;

        switch( tag ){
        case 31:
            chk = getResultOfULCheck( start, check );
            break;
        case 32:
            chk = getResultOfOLCheck( start, check );
            break;
        case 34:
            chk = getResultOfDLCheck( start, check );
            break;
        case 38:
            chk = getResultOfTABLECheck( start, check );
            break;
        default:
            chk = getResultOfDefaultCheck( start, check );
            break;
        }
        return chk;
    }

    /**
     * ULタグについて詳細なチェックを行う
     */
    private int getResultOfULCheck( int start, int chk )
    {
        int check =  getResultOfDefaultCheck( start, chk );

        if( check > 0 ){
            if( SLASH == 2 && PTAG_OFF > 0 ){
                PTAG_OFF--;
            } else if( SLASH == 1 ){
                PTAG_OFF++;
            }
        }
        return check;
    }

    /**
     * OLタグについて詳細なチェックを行う
     */
    private int getResultOfOLCheck( int start, int chk )
    {
        int check =  getResultOfDefaultCheck( start, chk );

        if( check > 0 ){
            if( SLASH == 2 && PTAG_OFF > 0 ){
                PTAG_OFF--;
            } else if( SLASH == 1 ){
                PTAG_OFF++;
            }
        }
        return check;
    }

    /**
     * DLタグについて詳細なチェックを行う
     */
    private int getResultOfDLCheck( int start, int chk )
    {
        int check =  getResultOfDefaultCheck( start, chk );

        if( check > 0 ){
            if( SLASH == 2 && PTAG_OFF > 0 ){
                PTAG_OFF--;
            } else if( SLASH == 1 ){
                PTAG_OFF++;
            }
        }
        return check;
    }

    /**
     * TABLEタグについて詳細なチェックを行う
     */
    private int getResultOfTABLECheck( int start, int chk )
    {
        int check =  getResultOfDefaultCheck( start, chk );

        if( check > 0 ){
            if( SLASH == 2 && PTAG_OFF > 0 ){
                PTAG_OFF--;
            } else if( SLASH == 1 ){
                PTAG_OFF++;
            }
        }
        return check;
    }

    /**
     * 標準的な検査を行う関数
     * 他のものを通さなくても, 最低限これだけ通せばok
     */
    private int getResultOfDefaultCheck( int start, int chk )
    {
        int check = chk;
        char c;

        // 行末までに終端タグがあるかどうかを調べる
        if( check > 0 ){
            // ここで'c'が指すものは, ' ', '>'のどちらかのハズ(コメントを除いて).
            c = input.charAt( start+check );
            for( ; c!='>'; check++ ){
                if( c == '\n' ){ return 0; }
                // ここで"c"が指すものは, ' ', '>'のどちらかのハズ(コメントを除いて).
                c = input.charAt( start+check );
            }
        }

        return check;
    }

    /**
     * HTMLタグ判別をするために利用するハッシュテーブル
     */
    private static Hashtable<String, Integer> map = new Hashtable<String, Integer>();

    /**
     * fillMap関数を実行して, タグをハッシュテーブルにセットする.
     * オブジェクト作成時, 自動で実行される.
     */
    static {
        fillMap();
    }

    /**
     * ハッシュテーブルに値をセットする関数.
     */
    private static void setTag( String k, Integer v )
    {
        map.put( k, v );
    }

    /**
     * 入力を許可しているタグ.
     * 値が無い場合はNullPointerExceptionを返す
     */
    private static void fillMap()
    {
        setTag(        "!--",  0 );
        setTag(         "H1",  1 );
        setTag(         "H2",  2 );
        setTag(         "H3",  3 );
        setTag(         "H4",  4 );
        setTag(         "H5",  5 );
        setTag(         "H6",  6 );
        setTag(       "FONT",  7 );
        setTag(   "BASEFONT",  8 );
        setTag(        "BIG",  9 );
        setTag(      "SMALL", 10 );
        setTag(          "B", 11 );
        setTag(          "I", 12 );
        setTag(          "U", 13 );
        setTag(          "S", 14 );
        setTag(         "TT", 15 );
        setTag(        "SUP", 16 );
        setTag(        "SUB", 17 );
        setTag(      "BLINK", 18 );
        setTag(          "P", 19 );
        setTag(         "BR", 20 );
        setTag(         "HR", 21 );
        setTag(     "CENTER", 22 );
        setTag(        "DIV", 23 );
        setTag( "BLOCKQUOTE", 24 );
        setTag(        "PRE", 25 );
        setTag(        "XMP", 26 );
        setTag(       "NOBR", 27 );
        setTag(        "WBR", 28 );
        setTag(   "MULTICOL", 29 );
        setTag(     "SPACER", 30 );
        setTag(         "UL", 31 );
        setTag(         "OL", 32 );
        setTag(         "LI", 33 );
        setTag(         "DL", 34 );
        setTag(         "DT", 35 );
        setTag(         "DD", 36 );
        setTag(          "A", 37 );
        setTag(      "TABLE", 38 );
        setTag(         "TR", 39 );
        setTag(         "TD", 40 );
        setTag(         "TH", 41 );
        setTag(    "CAPTION", 42 );
        setTag(        "IMG", 43 );
        setTag(        "MAP", 44 );
        setTag(       "AREA", 45 );
    }
}

getAnchorToUrlTagが正規表現で一致したとこを全部置換するような関数.
この正規表現を改造して, []これでくくられてたらリンク貼らないってできないかなって考えたけど,
自分の能力じゃ無理.


毎度, 思うけど関数名とか付けるの下手だなあと思う.
こうゆうとき英語が少しでもできれば洒落た名前を付けられるのに, と考えてしまう.