/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.xml;

import org.xml.sax.Attributes;
import java.util.List;
import java.util.ArrayList;

/**
 * 属性リストをあらわす、OGAttributes クラスを定義します。
 *
 * 属性リストは、キーと値のペアを、並び順で管理しているリストを保持しています。
 * 内部的には、 org.xml.sax.Attributes からの値の設定と、タブの属性の整列を行うための
 * 属性タブ、属性の改行の制御、属性の長さの制御 などを行います。
 *
 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
 *
 * @version  5.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK6.0,
 */
public class OGAttributes {
	/** システム改行コード **/
	public static final String	CR     = System.getProperty("line.separator");

	/** 属性の個数制限。この個数で改行を行う。 {@value} */
	public static final int		CR_CNT = 4;
	/** 属性の長さ制限。これ以上の場合は、改行を行う。 {@value} */
	public static final int		CR_LEN = 80;

	private final List<OGAtts>	attList = new ArrayList<OGAtts>();

//	private String	attTab	= CR + "  ";		// タグの前方スペース
//	private String	attTab	= "  ";				// 属性の前方スペース
	private boolean	useCR	= false;			// 属性の改行出力を行うかどうか。個数制限が１と同じ
	private int	maxValLen	= 0;				// 属性の名前の最大文字数
	private String	id		= null;				// 特別な属性。id で検索を高速化するため。

	/**
	 * デフォルトトコンストラクター
	 *
	 * 取りあえず、属性オブジェクトを構築する場合に使用します。
	 * 属性タブは、改行＋タブ 、属性リストは、空のリスト、属性改行は、false を初期設定します。
	 *
	 */
	public OGAttributes() {
		// Document empty method チェック対策
	}

	/**
	 * 属性タブ、属性リスト、属性改行の有無を指定してのトコンストラクター
	 *
	 * 属性タブ、属性リストに null を指定すると、デフォルトトコンストラクターの設定と
	 * 同じ状態になります。
	 *
	 * 注意 属性値の正規化は必ず行われます。
	 * 属性値に含まれるCR(復帰), LF(改行), TAB(タブ)は、 半角スペースに置き換えられます。
	 * XMLの規定では、属性の並び順は保障されませんが、SAXのAttributesは、XMLに記述された順番で
	 * 取得できていますので、このクラスでの属性リストも、記述順での並び順になります。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
	 *
	 * @param attri		属性リスト
	 */
//	public OGAttributes( final String attTab , final Attributes attri , final boolean useCR ) {
	public OGAttributes( final Attributes attri ) {
//		if( attTab != null ) {
////			this.attTab = CR + attTab + "  ";
//			this.attTab = attTab;
//		}
//		this.useCR  = useCR;

		int num = (attri == null)? 0 : attri.getLength();
		int maxLen = 0;
		for (int i = 0; i < num; i++) {
//			OGAtts atts = new OGAtts( attri.getQName(i),attri.getValue(i) );
			String key = attri.getQName(i);
			String val = attri.getValue(i);
			OGAtts atts = new OGAtts( key,val );
			attList.add( atts );
			maxLen = atts.maxKeyLen( maxLen );

			if( "id".equals( key ) ) { id = val; }		// 5.1.9.0 (2010/08/01)
		}

		maxValLen = maxLen;
	}

	/**
	 * 属性タブを設定します。
	 *
	 * 属性タブは、自身のエレメントの書き出し位置までの文字列を持っています。
	 * 属性を、そのまま、タグの横に出すと、１行に収まらないケースがあります。
	 * その場合、自身の位置＋タブを属性の書き出しに使用します。
	 * その文字列を定義しています。
	 *
	 * 引数は、自身のエレメントの書き出し位置です。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 廃止
	 *
	 * 内部的に、｢CR + attTab + タブ｣ に加工されます。
	 *
	 * @param	attTab	属性タブ
	 */
//	public void setAttrTab( final String attTab ) {
//		if( attTab != null ) {
////			this.attTab = CR + attTab + "\t";
//			this.attTab = attTab;
//		}
//	}

	/**
	 * 属性改行の有無を設定します。
	 *
	 * タグによって、属性が多くなったり、意味が重要な場合は、属性１つづつに改行を
	 * 行いたいケースがあります。
	 * 属性改行をtrue に設定すると、属性一つづつで、改行を行います。
	 *
	 * false の場合は、自動的な改行処理が行われます。
	 * これは、属性の個数制限(CR_CNT)を超える場合は、改行を行います。
	 *
	 * @param	useCR	属性改行の有無(true:１属性単位の改行)
	 * @see #CR_CNT
	 * @see #CR_LEN
	 */
	public void setUseCR( final boolean useCR ) {
		this.useCR  = useCR;
	}

	/**
	 * 属性リストの個数を取得します。
	 *
	 * @return	属性リストの個数
	 */
	public int size() {
		return attList.size();
	}

	/**
	 * 属性リストから、指定の配列番号の、属性キーを取得します。
	 *
	 * @param	adrs	配列番号
	 *
	 * @return	属性キー
	 */
	public String getKey( final int adrs ) {
		return attList.get(adrs).KEY ;
	}

	/**
	 * 属性リストから、指定の配列番号の、長さ補正が行われた属性キーを取得します。
	 *
	 * useCR=true の場合に、属性の改行が行われますが、そのときに、キーが縦に並びます。
	 * そして、値も縦に並ぶため、間の ｢＝｣記号の位置をそろえて、表示します。
	 * 属性リストの最大長さ＋１ になるように、キーの文字列にスペースを埋めます。
	 * これにより、属性を改行して表示しても、値の表示位置がそろいます。
	 *
	 * @param	adrs	配列番号
	 *
	 * @return	属性リスト群の長さ補正が行われた、属性キー＋空白文字列
	 */
//	private String getAlignKey( final int adrs ) {
//		return attList.get(adrs).KEY + SPACE.substring( attList.get(adrs).LEN,maxValLen ) ;
//	}

	/**
	 * 属性リストから、指定の配列番号の、属性値を取得します。
	 *
	 * @param	adrs	配列番号
	 *
	 * @return	属性値
	 */
	public String getVal( final int adrs ) {
		return attList.get(adrs).VAL ;
	}

	/**
	 * 属性リストから、指定の属性キーの、属性値を取得します。
	 *
	 * この処理は、属性リストをすべてスキャンして、キーにマッチする
	 * 属性オブジェクトを見つけ、そこから、属性値を取り出すので、
	 * パフォーマンスに問題があります。
	 * 基本的には、アドレス指定で、属性値を取り出すようにしてください。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規追加
	 *
	 * @param	key	属性キー
	 *
	 * @return	属性値
	 */
	public String getVal( final String key ) {
		String val = null;

		if( key != null ) {
			for( OGAtts atts : attList ) {
				if( key.equals( atts.KEY ) ) {
					val = atts.VAL;
					break;
				}
			}
		}

		return val;
	}

	/**
	 * 属性リストから、指定の属性キーの、アドレスを取得します。
	 *
	 * どちらかというと、キーの存在チェックに近い処理を行います。
	 * この処理は、属性リストをすべてスキャンして、キーにマッチする
	 * 属性オブジェクトを見つけ、そこから、属性値を取り出すので、
	 * パフォーマンスに問題があります。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規追加
	 *
	 * @param	key	属性キー
	 *
	 * @return	アドレス キーが存在しない場合は、-1 を返す。
	 */
	public int getAdrs( final String key ) {
		int adrs = -1;

		if( key != null ) {
			for( int i=0; i<attList.size(); i++ ) {
				if( key.equals( attList.get(i).KEY ) ) {
					adrs = i;
					break;
				}
			}
		}

		return adrs;
	}

	/**
	 * 属性リストから、id属性の、属性値を取得します。
	 *
	 * id属性 は、内部的にキャッシュしており、すぐに取り出せます。
	 * タグを特定する場合、一般属性のキーと値で選別するのではなく、
	 * id属性を付与して選別するようにすれば、高速に見つけることが可能になります。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規追加
	 *
	 * @return	id属性値
	 */
	public String getId() { return id; }

	/**
	 * 属性リストの、指定の配列番号に、属性値を設定します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
	 *
	 * @param	adrs	配列番号
	 * @param	val	属性値
	 */
	public void setVal( final int adrs , final String val ) {
		OGAtts atts = attList.remove(adrs) ;
		attList.add( adrs , new OGAtts( atts.KEY,val ) );

		if( "id".equals( atts.KEY ) ) { id = val; }		// 5.1.9.0 (2010/08/01)
	}

	/**
	 * 属性リストに、指定のキー、属性値を設定します。
	 * もし、属性リストに、指定のキーがあれば、属性値を変更します。
	 * なければ、最後に追加します。
	 *
	 * @og.rev 5.6.1.2 (2013/02/22) 新規追加
	 *
	 * @param	key	属性キー
	 * @param	val	属性値
	 */
	public void setVal( final String key , final String val ) {
		int adrs = getAdrs( key );
		if( adrs < 0 ) { add( key,val ); }
		else           { setVal( adrs,val ); }
	}

	/**
	 * 属性リストに、属性(キー、値のセット)を設定します。
	 *
	 * 属性リストの一番最後に、属性(キー、値のセット)を設定します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
	 *
	 * @param	key	属性リストのキー
	 * @param	val	属性リストの値
	 */
	public void add( final String key , final String val ) {

		OGAtts atts = new OGAtts( key,val );
		attList.add( atts );
		maxValLen = atts.maxKeyLen( maxValLen );

		if( "id".equals( key ) ) { id = val; }		// 5.1.9.0 (2010/08/01)
	}

	/**
	 * 指定のアドレスの属性リストに、属性(キー、値のセット)を設定します。
	 *
	 * 指定のアドレスの属性を置き換えるのではなく追加します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
	 *
	 * @param	adrs	属性リストのアドレス
	 * @param	key	属性リストのキー
	 * @param	val	属性リストの値
	 */
	public void add( final int adrs , final String key , final String val ) {

		OGAtts atts = new OGAtts( key,val );
		attList.add( adrs , atts );
		maxValLen = atts.maxKeyLen( maxValLen );

		if( "id".equals( key ) ) { id = val; }		// 5.1.9.0 (2010/08/01)
	}

	/**
	 * 指定のアドレスの属性リストから、属性情報を削除します。
	 *
	 * 指定のアドレスの属性を置き換えるのではなく追加します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
	 *
	 * @param	adrs	属性リストのアドレス
	 */
	public void remove( final int adrs ) {
//		attList.remove(adrs) ;

		OGAtts atts = attList.remove(adrs) ;

		if( "id".equals( atts.KEY ) ) { id = null; }		// 5.1.9.0 (2010/08/01)

		// 削除したキーが maxValLen だったとしても、再計算は、行いません。
		// 再計算とは、次の長さを見つける必要があるので、すべての OGAtts をもう一度
		// チェックする必要が出てくるためです。
	}

	/**
	 * オブジェクトの文字列表現を返します。
	 *
	 * 属性については、並び順は、登録順が保障されます。
	 *
	 * 属性は、３つのパターンで文字列化を行います。
	 *  ・useCR=true の場合
	 *     この場合は、属性を１行ずつ改行しながら作成します。属性キーは、
	 *     最大長＋１ でスペース埋めされて、整形されます。
	 *  ・useCR=false の場合
	 *     ・属性の個数制限(CR_CNT)単位に、改行が行われます。
	 *       これは、属性が右に多く並びすぎるのを防ぎます。
	 *     ・属性の長さ制限(CR_LEN)単位で、改行が行われます。
	 *       これは、たとえ、属性の個数が少なくても、文字列として長い場合は、
	 *       改行させます。
	 *
	 * @og.rev 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
	 * @og.rev 5.6.4.4 (2013/05/31) 改行処理の修正。attTabが、ゼロ文字列の場合の対応。
	 *
	 * @param	attTab	Nodeの階層を表す文字列。
	 * @return	このオブジェクトの文字列表現
	 * @see OGNode#toString()
	 * @see #setUseCR( boolean )
	 */
//	@Override
//	public String toString() {
	public String getText( final String attTab ) {
		StringBuilder buf = new StringBuilder();

		String crTab = (attTab.length() > 0) ? attTab : CR + "\t" ;

		if( useCR ) {
			for( int i=0; i<size(); i++ ) {
				OGAtts atts = attList.get(i);
//				buf.append( attTab );
				buf.append( crTab );
				buf.append( atts.getAlignKey( maxValLen ) ).append( " = " ).append( atts.QRT_VAL );
			}
	 		// 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
			buf.append( CR );
		}
		else {
			int crCnt = 0;
			int crLen = 0;
			for( int i=0; i<size(); i++ ) {
				OGAtts atts = attList.get(i);
				crCnt++ ;
				crLen += atts.LEN;
				if( i>0 && (crCnt > CR_CNT || crLen > CR_LEN) ) {
//					buf.append( attTab );
					buf.append( crTab );
					buf.append( atts.KEY ).append( "=" ).append( atts.QRT_VAL );
					crCnt = 0;
					crLen = 0;
				}
				else {
					buf.append( " " ).append( atts.KEY ).append( "=" ).append( atts.QRT_VAL );
				}
			}
//			buf.append( " " );
		}

		return buf.toString();
	}

	/**
	 * オブジェクトの文字列表現を返します。
	 *
	 * @og.rev 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
	 *
	 * @return	このオブジェクトの文字列表現
	 * @see OGNode#toString()
	 */
	@Override
	public String toString() {
		return getText( " " );
	}
}

/**
 * 属性キーと属性値を管理する クラス
 *
 * 属性自身は、属性キーと属性値のみで十分ですが、改行処理や文字列の長さ設定で、
 * 予め内部処理をしておきたいため、クラス化しています。
 *
 * 内部変数は、final することで定数化し、アクセスメソッド経由ではなく、直接内部変数を
 * 参照させることで、見易さを優先しています。
 */
final class OGAtts {
	/** 属性の長さをそろえるための空白文字の情報 **/
	private static final String	SPACE  = "                    ";	// 5.1.9.0 (2010/09/01) public ⇒ private へ変更

	/** 属性キー **/
	public		final String  KEY ;
	/** 属性値 **/
	public		final String  VAL ;
	private		final int     KLEN ;
				final int     LEN ;
				final String  QRT_VAL;

	/**
	 * 引数を指定して構築する、コンストラクター
	 *
	 * 属性キーと、属性値 を指定して、オブジェクトを構築します。
	 *
	 * @param	key	属性キー
	 * @param	val	属性値
	 */
	public OGAtts( final String key , final String val ) {
		KEY  = key;
//		VAL  = (val == null) ? "" : val ;
		VAL  = (val == null) ? "" : htmlFilter(val) ;
		KLEN = key.length();
		LEN  = KLEN + VAL.length();

		QRT_VAL = ( VAL.indexOf( '"' ) >= 0 ) ? "'" + VAL + "'" : "\"" + VAL + "\"" ;
	}

	/**
	 * キーの文字長さの比較で、大きい数字を返します。
	 *
	 * 属性キーの最大の文字列長を求めるため、引数の長さと、属性キーの長さを比較して、
	 * 大きな値の方を返します。
	 * この処理を、属性すべてに行えば、最終的に最も大きな値が残ることになります。
	 *
	 * @param	maxLen	属性キーの最大長さ
	 *
	 * @return	属性リスト群の長さ補正が行われた、属性キー＋空白文字列
	 */
	protected int maxKeyLen( final int maxLen ) {
		return (maxLen > KLEN) ? maxLen : KLEN ;
	}

	/**
	 * 長さ補正が行われた属性キーを取得します。
	 *
	 * useCR=true の場合に、属性の改行が行われますが、そのときに、キーが縦に並びます。
	 * そして、値も縦に並ぶため、間の ｢＝｣記号の位置をそろえて、表示します。
	 * 属性リストの最大長さ＋１ になるように、キーの文字列にスペースを埋めます。
	 * これにより、属性を改行して表示しても、値の表示位置がそろいます。
	 *
	 * @param	maxLen	属性キーの最大長さ
	 *
	 * @return	属性リスト群の長さ補正が行われた、属性キー＋空白文字列
	 */
	protected String getAlignKey( final int maxLen ) {
		return KEY + SPACE.substring( KLEN,maxLen ) ;
	}

	/**
	 * HTML上のエスケープ文字を変換します。
	 *
	 * HTMLで表示する場合にきちんとエスケープ文字に変換しておかないと
	 * Script を実行されたり、不要なHTMLコマンドを潜り込まされたりするため、
	 * セキュリティーホールになる可能性があるので、注意してください。
	 *
	 * ※ オリジナルは、org.opengion.fukurou.util.StringUtil#htmlFilter( String )
	 * ですが、ダブルクオート、シングルクオートの変換処理を省いています。
	 *
	 * @param	input HTMLエスケープ前の文字列
	 *
	 * @return	エスケープ文字に変換後の文字列
	 * @see	 org.opengion.fukurou.util.StringUtil#htmlFilter( String )
	 */
	private String htmlFilter( final String input ) {
		if( input == null || input.length() == 0 ) { return ""; }
		StringBuilder rtn = new StringBuilder();
		char ch;
		for(int i=0; i<input.length(); i++) {
			ch = input.charAt(i);
			switch( ch ) {
				case '<'  : rtn.append("&lt;");   break;
				case '>'  : rtn.append("&gt;");   break;
		//		case '"'  : rtn.append("&quot;"); break;
		//		case '\'' : rtn.append("&#39;");  break;
				case '&'  : rtn.append("&amp;");  break;
				default   : rtn.append(ch);
			}
		}
		return( rtn.toString() );
	}
}
