/*********************************************************************
* All Rights reserved,Copyright (c) K-Opticom
**********************************************************************
*＜プログラム内容＞
*   システム名      ：eo顧客基幹システム
*   モジュール名    ：JCCMapConvXml
*   ソースファイル名：JCCMapConvXml.java
*   作成者          ：富士通
*   日付            ：2011年04月01日
*＜機能概要＞
*  MAPオブジェクトXMLデータ変換部品です。
*  所定の形式のHashMap型をXML形式に変換、
*  またはXML形式のデータをHashMap型に変換する機能を提供します。
*＜修正履歴＞
*   バージョン  修正日       修正者      修正内容
*   v1.00.00    2011/05/01   FJ)今川     新規作成
*
**********************************************************************/

package eo.common.util;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.InputSource;

//import com.fujitsu.futurity.web.x31.X31BWebBusinessLogic;

import eo.common.util.JCCFrameworkException;


public class JCCMapConvXml extends DefaultHandler implements LexicalHandler
{
	/** 文字エンコーディング格納用キー */
	public static final String KEY_ENCODING = "ENCODING";

	/** 要素のタグ名格納用キー */
	public static final String KEY_ELEMENT = "ELEMENT";

	/** 要素の属性格納用キー */
	public static final String KEY_ATTRIBUTE = "ATTRIBUTE";

	/** 要素の値データ格納用キー */
	public static final String KEY_VALUE = "VALUE";

	/** 文字間隔指定用 */
	private static final String STRING_SPACE = "\t";

	/** 改行コード */
	private static final String lineSeparator = "\n";

	/** 【Mapオブジェクト→XML変換用】要素タグ格納スタック */
	private Stack<String> qNameStack = null;

	/** 【XML→Mapオブジェクト変換用】Map(要素タグ、属性、値のデータを保持)を格納するスタック */
	private Stack<HashMap<String, Object>> mapStack = null;

	/** 【XML→Mapオブジェクト変換用】スタックからPOPした最終のMapを格納するスタック */
	private HashMap<String, Object> lastPopMap = null;
	
	/** 【XML→Mapオブジェクト変換用】文字エンコーディング保管用 */
	private String xmlEncoding = "";

	/**
	 * MapオブジェクトからXMLへの変換処理です。<br>
	 * Mapオブジェクトを解析してXMLに変換します。<br>
	 * <PRE>HashMapの最上位データ構造は以下の形式になります。
	 *    キー              値                                           型
	 * -----------  -----------------    --------------------------------------------------------------------------
	 * "ENCODING"   XML文字エンコード    String
	 * "ELEMENT"    要素名               String
	 * "ATTRIBUTE"  属性                 ArrayList&lt;HashMap&lt;String, String&gt;&gt;
	 *                                   要素に対する1つの属性のキーと値は1つのHashMap&lt;String, String&gt;にセットします。
	 *                                   HashMapはArrayListにセットします。
	 *                                   属性が複数ある場合、1つずつHashMapにしてArrayListにセットします。
	 * "VALUE"      値または子要素       ArrayList&lt;Object&gt;
	 *                                   要素に対する値または子要素をArrayListにセットします。
	 *                                   値の場合はString、子要素の場合はHashMap&lt;String, Object&gt;型でセットします。
	 *                                   親要素と同様にキーを"ELEMENT"として、子となる要素名をセットします。
	 *                                   属性、値も親要素と同様に"ATTRIBUTE"、"VALUE"をキーとしてセットします。
	 * 
	 * 設定例)
	 * &lt;?xml version="1.0" encoding="shift_jis"?&gt;
	 * &lt;RESPONSE&gt;
	 *     &lt;RESULT CODE="1"/&gt;
	 *     &lt;STATUS&gt;99999&lt;/STATUS&gt;
	 *     &lt;MESSAGE NO="1"&gt;メッセージ&lt;/MESSAGE&gt;
	 * &lt;/RESPONSE&gt;
	 *      ↓
	 *      ↓上記のXMLを生成したい場合、以下のHashMapのデータ構造とする必要があります。
	 *      ↓ 
	 * HashMapのデータ構造
	 *    キー              値
	 * ---------- ------------------------
	 * "ENCODING"  "shift_jis"
	 * "ELEMENT"   "RESPONSE"
	 * "VALUE"     ArrayList&lt;Object&gt; ---> ArrayListに格納されるHashMap
	 * 
	 *                                       HashMap
	 *                                    キー                   値
	 *                                 ----------- ---------------------------------
	 *                                 "ELEMENT"   "RESULT"
	 *                                 "ATTRIBUTE" ArrayList&lt;HashMap&lt;String, String&gt;&gt; ---> ArrayListに格納されるHashMap
	 *                                                                                         HashMap
	 *                                                                                        キー      値
	 *                                                                                    ----------- -------
	 *                                                                                    "CODE"       "1"
	 * 
	 *                                       HashMap
	 *                                    キー                   値
	 *                                 ----------- ---------------------------------
	 *                                 "ELEMENT"   "STATUS"
	 *                                 "VALUE"     ArrayList&lt;Object&gt;                  ---> ArrayListに格納されるObject
	 *                                                                                         値(String)
	 *                                                                                          "99999"
	 * 
	 *                                       HashMap
	 *                                    キー                   値
	 *                                 ----------- ---------------------------------
	 *                                 "ELEMENT"   "MESSAGE"
	 *                                 "ATTRIBUTE" ArrayList&lt;HashMap&lt;String, String&gt;&gt;  ---> ArrayListに格納されるHashMap
	 *                                                                                         HashMap
	 *                                                                                        キー      値
	 *                                                                                    ----------- -------
	 *                                                                                      "NO"        "1"
	 * 
	 *                                 "VALUE"     ArrayList&lt;Object&gt;                  ---> ArrayListに格納されるObject
	 *                                                                                         値(String)
	 *                                                                                          "メッセージ"
	 * </PRE>
	 * 
	 * @param map XMLデータを格納したMapオブジェクト
	 * @return XMLデータ
	 * @throws JCCFrameworkException
	 */
	public String getParseMapToXml(HashMap<String, Object> map)
	{
		StringBuffer sbXml = null;

		//----------------------------
		// 入力パラメータチェック
		//----------------------------
		checkInputpParameter(map);
		
		sbXml = new StringBuffer();
		qNameStack = new Stack<String>();

		//----------------------------
		// 文字エンコードの取得・設定
		//----------------------------
		// デフォルト文字エンコードを設定
		sbXml.append("<?xml version=\"1.0\" encoding=\"" + map.get(KEY_ENCODING) + "\"?>");
		sbXml.append(lineSeparator);

		//----------------------------
		// Map→XML本文の展開
		//----------------------------
		parseMapToXml(map, sbXml);
		return sbXml.toString();
	}

	/**
	 * 【Mapオブジェクト→XML変換用】入力パラメータチェックをする処理です。
	 * <br>
	 * @param map XML変換対象Mapオブジェクト
	 * @throws JCCFrameworkException
	 */
	private void checkInputpParameter(HashMap<String, Object> map) throws JCCFrameworkException
	{
	    if (null == map)
	    {
			throw new JCCFrameworkException("Mapオブジェクトが設定されていません。");
		}
	    
	    if (!map.containsKey(KEY_ENCODING))
	    {
			throw new JCCFrameworkException("文字エンコーディングが設定されていません。");
		}
	}
	
	/**
	 * MapオブジェクトからXMLへの変換処理です。<br>
	 * Mapオブジェクトを解析してXMLに変換します。<br>
	 * @param xml XML形式データ
	 * @return map XMLを格納したMapオブジェクト
	 * @throws JCCFrameworkException
	 */
	@SuppressWarnings("unchecked")
	private void parseMapToXml(HashMap<String, Object> map, StringBuffer sbXml)
	{
		//----------------------------
		// 要素の開始タグ取得
		//----------------------------
		String qName = "";
		if (!map.containsKey(KEY_ELEMENT))
		{
			// エラー
			throw new JCCFrameworkException("要素の開始タグの指定がありません。開始タグはELEMENTをキーにしてセットする必要があります。");
		}	
		qName = (String)map.get(KEY_ELEMENT);
		// コメント文でなければ開始タグ
		if (!qName.startsWith("<!--")){
			sbXml.append("<");
		}
		sbXml.append(qName);
		// コメント文の場合は属性、値はないので処理終了
		if (qName.startsWith("<!--")){
			return;
		}

		//----------------------------
		// 属性を取得
		//----------------------------
		if (map.containsKey(KEY_ATTRIBUTE))
		{
			ArrayList<HashMap<String, String>> attrList = (ArrayList<HashMap<String, String>>)map.get(KEY_ATTRIBUTE);
			Iterator<HashMap<String, String>> ite = attrList.iterator();
			while(ite.hasNext()) {
				HashMap<String, String> attrMap =  (HashMap<String, String>)ite.next();
				Iterator<String> attrIte = attrMap.keySet().iterator();
				while(attrIte.hasNext()){
					String key = (String)attrIte.next();
// 2012/07/23 FST)arata 属性の値をサニタイジング start
					String val = JCCSanitizingUtil.escapeString(attrMap.get(key));
// 2012/07/23 FST)arata 属性の値をサニタイジング end
					sbXml.append(" ").append(key).append("=").append("\"").append(val).append("\"");
				}
			}
		}
		// 属性まで取得後スタックへPUSH
		qNameStack.push(qName);
		
		//----------------------------
		// 値を取得
		//----------------------------
		if (map.containsKey(KEY_VALUE))
		{
			boolean tagflg = false;
			// 親要素のタグ閉じ
			sbXml.append(">");
			ArrayList<HashMap<String, Object>> valueList = (ArrayList<HashMap<String, Object>>)map.get(KEY_VALUE);
			Iterator<HashMap<String, Object>> ite = valueList.iterator();
			while(ite.hasNext()) {
				Object value =  (Object)ite.next();
				// 子要素の場合
				if (value instanceof HashMap)
				{
					// 要素タグあり
					tagflg = true;
					// 親要素の後に改行をセットして、子要素出力位置を調整
					sbXml.append(lineSeparator);
					for (int ii = 0; ii < qNameStack.size(); ii++)
					{
						sbXml.append(STRING_SPACE);
					}	
					// 子要素のHashMapをXmlへ展開
					parseMapToXml((HashMap<String, Object>)value, sbXml);
				}
				// 値の場合
				else if(value instanceof String)
				{
// 2012/07/23 FST)arata 要素の値をサニタイジング start
					sbXml.append(JCCSanitizingUtil.escapeString((String)value));
// 2012/07/23 FST)arata 要素の値をサニタイジング end
				}
			}
			
			// 子要素の場合、出力したら親の階層に戻す。
			String pop = (String)qNameStack.pop();
			if(tagflg) {
				sbXml.append(lineSeparator);
				for (int ii = 0; ii < qNameStack.size(); ii++)
				{
					sbXml.append(STRING_SPACE);
				}	
			}
			sbXml.append("</").append(pop).append(">");
		}
		// 要素に値がない場合
		else
		{
			// タグを閉じるパターン
			// sbXml.append("/>");
			// 終了タグ
			// qNameStack.pop();

			// 終了タグを追加するパターン
			sbXml.append("></").append(qNameStack.pop()).append(">");
		}
	}
	
	/**
	 * XMLからMapオブジェクトへの変換処理です。<br>
	 * XMLを解析してMapオブジェクトに変換します。<br>
	 * XMLから生成されるMapオブジェクトのデータ構造は「getParseMapToXml MapオブジェクトからXMLへの変換処理」を参照ください。<br>
	 * 
	 * @param xml XML形式データ
	 * @return map XMLを格納したMapオブジェクト
	 * @throws SAXException 
	 * @throws ParserConfigurationException 
	 * @throws IOException 
	 */
	public HashMap<String, Object> getParseXmlToMap(String xml) throws SAXException, ParserConfigurationException, IOException 
	{
		//----------------------------
		// 入力チェック
		//----------------------------
		checkInputpParameter(xml);

		//----------------------------
		// SAXパーサー取得
		//----------------------------
		SAXParserFactory spf = SAXParserFactory.newInstance();
		spf.setNamespaceAware(false);
		spf.setValidating(false);
		spf.setXIncludeAware(false);
		SAXParser sp = spf.newSAXParser();

		//----------------------------
		// コメント取得用
		//----------------------------
		// 変換対象 XML文字列取得・変換
		sp.setProperty("http://xml.org/sax/properties/lexical-handler", this);

		
		//----------------------------
		// 文字エンコーディング取得用
		//----------------------------
		// ロケーターの拡張設定
		// sp.getXMLReader().setFeature("http://xml.org/sax/features/use-locator2", true);
		// <?xml ・・・ ?> よりencodingの値を取出す
		int staXml = xml.indexOf("<?xml");
		int endXml = xml.indexOf("?>");
		
		if (-1 != staXml && -1 != endXml)
		{
			// <?xml ・・・ ?> 取出し
			String xmlInfo = xml.substring(staXml, endXml + 2);

			// encoding="・・・" 取出し
			int staEncodeInfo = xmlInfo.indexOf("encoding");
			if (-1 != staEncodeInfo)
			{
				// encodingの次の文字出力("?>"を除く)
				String encodeInfo = xmlInfo.substring(staEncodeInfo + 8, xmlInfo.length() - 2);

				// "文字エンコーティング"の取出し
				int staEncoding = encodeInfo.indexOf("\"", 1);
				int endEncoding = encodeInfo.indexOf("\"", 2);
				if (-1 != staEncoding && -1 != endEncoding)
				{
					xmlEncoding = encodeInfo.substring(staEncoding + 1, endEncoding);
				}
			}	
		}
		
		//----------------------------
		// XML→Mapオブジェクト変換
		//----------------------------
		InputStream is = new ByteArrayInputStream(xml.getBytes(xmlEncoding));
		sp.parse(new InputSource(is), this);
		return lastPopMap;
	}
	
	
	/**
	 * 【XML→Mapオブジェクト変換用】入力パラメータチェックをする処理です。
	 * <br>
	 * @param xml XMLデータ
	 * @throws JCCFrameworkException
	 */
	private void checkInputpParameter(String xml) throws JCCFrameworkException
	{
	    if (null == xml || "".equals(xml))
	    {
			throw new JCCFrameworkException("XMLが設定されていません。");
		}
	}
	

	
    /**
     * 【XML→Mapオブジェクト変換用】XMLドキュメント開始処理です。
     * <br>
     */
	@Override
	public void startDocument()
	{
		// 要素の階層を積むスタック
		mapStack = new Stack<HashMap<String, Object>>();
	}
	
    /**
     * 【XML→Mapオブジェクト変換用】XMLドキュメント終了処理です。
     * <br>
     */
	@Override
	public void endDocument()
	{
	}
    
    /**
     * 【XML→Mapオブジェクト変換用】XMLの開始要素をSAXParserで解析します。<br>
     * 
     * @param uri uri
     * @param localName localName
     * @param qName 要素のタグ名
     * @param attrs 属性
     * 
     */
	@SuppressWarnings("unchecked")
	@Override
	public void startElement(String uri,String localName, String qName, Attributes attrs)
	{
		HashMap<String, Object> map = new HashMap<String, Object>();
		//-------------------------------------------
		// スタックに要素のMapオブジェクト退避
		//-------------------------------------------
		// 要素未登録の場合
		if (mapStack.size() == 0)
		{
			// 新しくスタックに積む
			map.put(KEY_ENCODING, xmlEncoding);
			map.put(KEY_ELEMENT, qName);
			mapStack.push(map);
		}
		// 既存要素登録あり
		else
		{
			// 親要素取得
			HashMap<String, Object> parentMap = (HashMap<String, Object>)mapStack.peek();
			// 親要素よりVALUEキーで格納先オブジェクト取得
			if (!parentMap.containsKey(KEY_VALUE))
			{
				// なければ格納先新規登録
				ArrayList<Object> valueList = new ArrayList<Object>();
				parentMap.put(KEY_VALUE, valueList);
			}

			// 要素を親要素のVALUE格納先オブジェクトに登録
			map.put(KEY_ELEMENT, qName);
			((ArrayList<Object>)parentMap.get(KEY_VALUE)).add(map);
			// 子要素をスタックに退避
			mapStack.push(map);
		}
		//-------------------------------------------
		// 属性の設定
		//-------------------------------------------
		String attrName = null;
		String attrVal  = null;
		HashMap<String, String> attr = null;
		ArrayList<HashMap<String, String>> attrList = null;
		// 属性のHashMapはArrayListに格納する。
		if (0 < attrs.getLength()) {
			attrList = new ArrayList<HashMap<String, String>>();
		}

		for(int ii = 0; ii < attrs.getLength(); ii++ ){
			// 1つずつ属性をHashMapに格納する。
			attr = new HashMap<String, String>();
			// 属性名称
			attrName = attrs.getQName(ii);
			// 属性値
			attrVal  = attrs.getValue(ii);
			// 属性→Map
			attr.put(attrName, attrVal);
			// Map→ArrayList
			attrList.add(attr);
		}

		// ArrayListに属性が設定されていればMapオブジェクトに格納
		if (null != attrList && 0 < attrList.size()){
			map.put(KEY_ATTRIBUTE, attrList);
		}

	}
	
    /**
     * 【XML→Mapオブジェクト変換用】XMLの終了要素をSAXParserで解析します。<br>
     * 
     * @param uri 名前空間URI
     * @param localName ロケール名
     * @param qName 要素のタグ名
     * 
     */
	@SuppressWarnings("unchecked")
	@Override
	public void endElement(String uri, String localName, String qName)
	{
		// 要素のMapオブジェクト取得
		lastPopMap = (HashMap<String, Object>)mapStack.pop();

		// MapオブジェクトにVALUE格納先オブジェクトがなければ処理しない
		if (!lastPopMap.containsKey(KEY_VALUE))
		{
			return;
		}
		
		// VALUE格納先オブジェクト取得
		ArrayList<Object> valueList = (ArrayList<Object>)lastPopMap.get(KEY_VALUE);

		// 値リスト数が1の場合は処理しない
		if (valueList.size() <= 1)
		{
			return;
		}	

		// 値リスト数に2以上設定されているものは子要素とみなし
		// 子要素が存在する状態での文字列は不要なスペースの値であるため削除する。
		for (int ii = 0; ii < valueList.size(); ii++ )
		{
			
			Object value = valueList.get(ii);
			if (value instanceof String && value.equals(""))
			{
				valueList.remove(ii);
				ii--;
// 2012/07/23 FST)arata String型でかつ値が存在する場合は、0番目に全てを入れる start
// 要素の値がサニタイジング対象の文字の場合に配列で分かれて取得されるため
			}else if(value instanceof String && !value.equals(""))
			{
				if(ii != 0)
				{
					valueList.set(0, (String)valueList.get(0) + (String)valueList.get(ii));
					valueList.remove(ii);
					ii--;
				}
// 2012/07/24 FST)arata 0番目はトリムを行う。（末端の改行が消されるため、足並みを揃える） start
				else
				{
					valueList.set(0, ((String)valueList.get(0)).trim());
				}
// 2012/07/24 FST)arata 0番目はトリムを行う。（末端の改行が消されるため、足並みを揃える） end
			}
// 2012/07/23 FST)arata String型でかつ値が存在する場合は、0番目に全てを入れる end
		}
	}

	
    /**
     * 【XML→Mapオブジェクト変換用】XMLの要素のデータ(値)をSAXParserで取得します。<br>
     * 
     * @param ch XML文字列データ
     * @param start 文字データ開始位置
     * @param length 文字データサイズ
     * 
     */
	@SuppressWarnings("unchecked")
	@Override
	public void characters(char[] ch, int start, int length)
	{
		// 文字列の値が属する要素のMapオブジェクト取得
		HashMap<String, Object> map = (HashMap<String, Object>)mapStack.peek();
		// 対象文字列取出
		String value = String.copyValueOf(ch, start, length).trim();
// 2012/07/24 FST)arata 文字がブランク以外の場合はトリムを行わない。 start
		if(value.length() != 0)
		{
			value = String.copyValueOf(ch, start, length);
		}
// 2012/07/24 FST)arata 文字がブランク以外の場合はトリムを行わない。 end
		// VALUEの格納先オブジェクト値への文字列の設定
		if (!map.containsKey(KEY_VALUE))
		{
			map.put(KEY_VALUE, new ArrayList<Object>());
		}
		((ArrayList<Object>)map.get(KEY_VALUE)).add(value);
	}

    /**
     * 【XML→Mapオブジェクト変換用】XMLのコメントをSAXParserで取得します。<br>
     * 
     * @param ch XML文字列データ
     * @param start 文字データ開始位置
     * @param length 文字データサイズ
     * 
     */
	@SuppressWarnings("unchecked")
	@Override
	public void comment(char[] ch, int start, int length)
	{
		// 文字列の値が属する要素のMapオブジェクト取得
		HashMap<String, Object> map = (HashMap<String, Object>)mapStack.peek();
		// 対象文字列取出
		String value = String.copyValueOf(ch, start, length).trim();
		// VALUEの格納先オブジェクト値への文字列の設定
		if (!map.containsKey(KEY_VALUE))
		{
			map.put(KEY_VALUE, new ArrayList<Object>());
		}
		HashMap<String, Object> comm = new HashMap<String, Object>();
		comm.put(KEY_ELEMENT, "<!--" + value + "-->");
		((ArrayList<Object>)map.get(KEY_VALUE)).add(comm);
	}

	@Override
	public void endCDATA() throws SAXException {
		// 自動生成されたメソッドスタブ
		
	}

	@Override
	public void endDTD() throws SAXException {
		// 自動生成されたメソッドスタブ
		
	}

	@Override
	public void endEntity(String arg0) throws SAXException {
		// 自動生成されたメソッドスタブ
		
	}

	@Override
	public void startCDATA() throws SAXException {
		// 自動生成されたメソッドスタブ
		
	}

	@Override
	public void startDTD(String arg0, String arg1, String arg2)
			throws SAXException {
		// 自動生成されたメソッドスタブ
		
	}

	@Override
	public void startEntity(String arg0) throws SAXException {
		// 自動生成されたメソッドスタブ
		
	}
}