package org.maachang.comet.httpd.engine.session ;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashMap;

import org.maachang.util.ConvertParam;

/**
 * １つのセッションを表わす.
 *
 * @version 2007/08/20
 * @author  masahito suzuki
 * @since   MaachangComet 1.00
 */
public class HttpdSession {
    
    /**
     * セッションを保持する名前.
     */
    public static final String SESSION_NAME = "maachang-session" ;
    
    /**
     * セッションID.
     */
    private String sessionId = null ;
    
    /**
     * セッション要素群.
     */
    private HashMap<String,String> elements = null ;
    
    /**
     * 生成時間.
     */
    private Date createDate = null ;
    
    /**
     * 更新情報.
     */
    private Date updateDate = null ;
    
    /**
     * コンストラクタ.
     */
    private HttpdSession() {
    }
    
    /**
     * コンストラクタ.
     * <BR><BR>
     * 対象条件を設定してオブジェクトを生成します.
     * <BR>
     * @param sessionId 対象のセッションIDを設定します.
     */
    public HttpdSession( String sessionId ) {
        this.sessionId = sessionId ;
        this.createDate = new Date() ;
        this.updateDate = new Date() ;
    }
    
    /**
     * セッション要素を設定.
     * <BR><BR>
     * セッション要素を設定します.
     * <BR>
     * @param key セッションキーを設定します.
     * @param value セッション要素を設定します.
     */
    public synchronized void setElement( String key,String value ) {
        if( key == null || key.length() <= 0 || value == null || value.length() <= 0 ) {
            return ;
        }
        if( elements == null ) {
            elements = new HashMap<String,String>() ;
        }
        elements.put( key,value ) ;
    }
    
    /**
     * セッション要素を削除.
     * <BR><BR>
     * セッション要素を削除します.
     * <BR>
     * @param key セッションキーを設定します.
     */
    public synchronized void removeElement( String key ) {
        if( key == null && key.length() <= 0 ) {
            return ;
        }
        this.updateDate = new Date() ;
        if( elements != null ) {
            elements.remove( key ) ;
        }
    }
    
    /**
     * セッション要素を取得.
     * <BR><BR>
     * セッション要素を取得します.
     * <BR>
     * @param key セッションキーを設定します.
     * @return String セッション要素が返されます.
     */
    public synchronized String getElement( String key ) {
        if( key == null && key.length() <= 0 ) {
            return null ;
        }
        this.updateDate = new Date() ;
        if( elements != null ) {
            return elements.get( key ) ;
        }
        return null ;
    }
    
    /**
     * セッション格納数を取得.
     * <BR><BR>
     * セッション格納数が返されます.
     * <BR>
     * @return int セッション格納数が返されます.
     */
    public synchronized int size() {
        if( elements != null ) {
            return elements.size() ;
        }
        return 0 ;
    }
    
    /**
     * セッションIDを取得.
     * <BR><BR>
     * セッションIDを取得します.
     * <BR>
     * @return String セッションIDが返されます.
     */
    public synchronized String getSessionId() {
        return sessionId ;
    }
    
    /**
     * 生成時間を取得.
     * <BR><BR>
     * 生成時間を取得します.
     * <BR>
     * @return Date 生成時間が返されます.
     */
    public synchronized Date getCreateDate() {
        return this.createDate ;
    }
    
    /**
     * 更新時間を取得.
     * <BR><BR>
     * 更新時間を取得します.
     * <BR>
     * @return Date 生成時間が返されます.
     */
    public synchronized Date getUpdateDate() {
        return this.updateDate ;
    }
    
    /**
     * 文字情報として出力.
     * <BR>
     * @return String 文字列として出力します.
     */
    public synchronized String toString() {
        StringBuilder buf = new StringBuilder() ;
        buf.append( " sessionId:" ).append( sessionId ) ;
        buf.append( " createTime:" ).append( createDate ) ;
        buf.append( " updateTime:" ).append( createDate ) ;
        buf.append( " elements: {" ) ;
        if( elements.size() > 0 ) {
            Object[] key = elements.keySet().toArray() ;
            int len = key.length ;
            for( int i = 0 ; i < len ; i ++ ) {
                if( i != 0 ) {
                    buf.append( " ," ) ;
                }
                buf.append( " key:" ).append( key[ i ] ) ;
                buf.append( " value:" ).append( elements.get( ( String )key[ i ] ) ) ;
            }
        }
        buf.append( " }" ) ;
        return buf.toString() ;
    }
    
    /**
     * １つのセッション情報をロード.
     * <BR><BR>
     * １つのセッション情報を保存します.
     * <BR>
     * @param read 読み込み対象のストリームを設定します.
     * @return HttpdSession 生成されたセッション情報が返されます.
     * @exception Exception 例外.
     */
    protected static final HttpdSession loadSession( InputStream in )
        throws Exception {
        // シーケンスID.
        byte[] bin = new byte[ 4 ] ;
        int len = in.read( bin ) ;
        if( len == -1 ) {
            // ファイルの終了を示す.
            return null ;
        }
        else if( len != 4 ) {
            throw new IOException( "Sessionロードに失敗しました" ) ;
        }
        len = ConvertParam.convertInt( 0,bin ) ;
        if( len < 0 ) {
            throw new IOException( "Sessionロード中に不正なシーケンスID[" + len + "]を検出しました" ) ;
        }
        // セッションIDを取得.
        if( in.read( bin ) != 4 ) {
            throw new IOException( "Sessionロードに失敗しました" ) ;
        }
        len = ConvertParam.convertInt( 0,bin ) ;
        if( len <= 0 ) {
            throw new IOException( "Sessionロード中に不正なセッションID長を検出しました" ) ;
        }
        bin = new byte[ len ] ;
        if( in.read( bin ) != len ) {
            throw new IOException( "Sessionロードに失敗しました" ) ;
        }
        HttpdSession session = new HttpdSession() ;
        session.sessionId = new String( bin,"UTF8" ) ;
        // 生成日付を取得.
        bin = new byte[ 8 ] ;
        if( in.read( bin ) != 8 ) {
            throw new IOException( "Session["+session.sessionId+"]ロードに失敗しました" ) ;
        }
        session.createDate = new Date( ConvertParam.convertLong( 0,bin ) ) ;
        // 更新時間を取得.
        if( in.read( bin ) != 8 ) {
            throw new IOException( "Session["+session.sessionId+"]ロードに失敗しました" ) ;
        }
        session.updateDate = new Date( System.currentTimeMillis() - ConvertParam.convertLong( 0,bin ) ) ;
        bin = null ;
        // 各セッション内容を取得.
        for( ;; ) {
            // キー名を取得.
            bin = new byte[ 4 ] ;
            len = in.read( bin ) ;
            if( len == -1 ) {
                // ファイルの終了を示す.
                break ;
            }
            if( len != 4 ) {
                throw new IOException( "Session["+session.sessionId+"]ロードに失敗しました" ) ;
            }
            len = ConvertParam.convertInt( 0,bin ) ;
            if( len == 0 ) {
                // 1つのセッション終了を示す.
                break ;
            }
            if( len <= 0 ) {
                throw new IOException( "Session["+session.sessionId+"]ロード中に不正なキー名長を検出しました" ) ;
            }
            bin = new byte[ len ] ;
            if( in.read( bin ) != len ) {
                throw new IOException( "Session["+session.sessionId+"]ロードに失敗しました" ) ;
            }
            String key = new String( bin,"UTF8" ) ;
            // 要素を取得.
            bin = new byte[ 4 ] ;
            if( in.read( bin ) != 4 ) {
                throw new IOException( "Session["+session.sessionId+"]ロードに失敗しました" ) ;
            }
            len = ConvertParam.convertInt( 0,bin ) ;
            if( len <= 0 ) {
                throw new IOException( "Session["+session.sessionId+"]ロード中に不正なシリアライズ長を検出しました" ) ;
            }
            bin = new byte[ len ] ;
            if( in.read( bin ) != len ) {
                throw new IOException( "Session["+session.sessionId+"]ロードに失敗しました" ) ;
            }
            String val = getObject( bin ) ;
            bin = null ;
            if( session.elements == null ) {
                session.elements = new HashMap<String,String>() ;
            }
            session.elements.put( key,val ) ;
        }
        return session ;
    }
    
    /**
     * １つのセッション情報を保存.
     * <BR><BR>
     * １つのセッション情報を保存します.
     * <BR>
     * @param id 対象のIDを設定します.
     * @param write 書き込み先のストリームを設定します.
     * @exception Exception 例外.
     */
    protected synchronized void saveSessoin( int id,OutputStream out )
        throws Exception {
        if( elements != null ) {
            out.write( new byte[]{ 0,0,0,0 } ) ;// セッションの先頭を示す.
            out.write( ConvertParam.convertInt( id ) ) ;// シーケンスIDを付加.
            byte[] bin = sessionId.getBytes( "UTF8" ) ;
            out.write( ConvertParam.convertInt( bin.length ) ) ;// セッションID長を設定.
            out.write( bin ) ;// セッションIDを設定.
            out.write( ConvertParam.convertLong( createDate.getTime() ) ) ;// 生成時間を設定.
            out.write( ConvertParam.convertLong(
                System.currentTimeMillis() - updateDate.getTime() ) ) ;// 現在と更新時間の差異を設定.
            bin = null ;
            // 各セッション内情報を保存(Serializableできないものは、保存しない).
            int len = elements.size() ;
            if( len > 0 ) {
                Object[] names = elements.keySet().toArray() ;
                for( int i = 0 ; i < len ; i ++ ) {
                    try {
                        String name = ( String )names[ i ] ;
                        if( name == null || name.length() <= 0 ) {
                            continue ;
                        }
                        byte[] v = convertBinary( elements.get( name ) ) ;
                        if( v == null || v.length <= 0 ) {
                            continue ;
                        }
                        bin = name.getBytes( "UTF8" ) ;
                        out.write( ConvertParam.convertInt( bin.length ) ) ;// 要素名長を設定.
                        out.write( bin ) ;// 要素名を設定.
                        bin = null ;
                        out.write( ConvertParam.convertInt( v.length ) ) ;// シリアライズ長を設定.
                        out.write( v ) ;// シリアライズ内容を設定.
                        v = null ;
                    } catch( Exception e ) {
                    } finally {
                        bin = null ;
                    }
                }
            }
        }
    }
    
    /**
     * 1つのオブジェクトをシリアライズ.
     */
    private static final byte[] convertBinary( String value ) {
        if( value == null || value.length() <= 0 ) {
            return null ;
        }
        byte[] ret = null ;
        try {
            ret = value.getBytes( "UTF8" ) ;
        } catch( Exception e ) {
            ret = null ;
        }
        return ret ;
    }
    
    /**
     * 対象バイナリから、オブジェクトを復元.
     */
    private static final String getObject( byte[] bin ) {
        if( bin == null || bin.length <= 0 ) {
            return null ;
        }
        String ret = null ;
        try {
            ret = new String( bin,"UTF8" ) ;
        } catch( Exception e ) {
            ret = null ;
        }
        return ret ;
    }
}

