/*
 * 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.transfer;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException ;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.opengion.fukurou.db.Transaction;
import org.opengion.fukurou.util.ApplicationInfo;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.URLConnect;
import static org.opengion.fukurou.util.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * 伝送要求に対して、HTTP経由でデータを読取します。
 *
 * 読取方法により読み取ったデータをPOSTデータとして送信し、リモートホスト上で
 * 伝送処理を実行します。
 *
 * 処理の流れとしては以下のようになります。
 * ①HTTPで読取処理を実行するためのサーブレットを呼び出す。
 * ②①で呼び出しされたサーブレットが、読取処理を実行するためのオブジェクトを生成し、
 *   読取処理を実行する。
 * ③読取した結果XMLをパースし、その読取データを伝送オブジェクトに渡し伝送処理を実行する。
 * ④伝送処理実行後、③の結果XMLから得られる更新キー(配列)をPostデータとして渡し、
 *   HTTP経由で後処理(完了処理またはエラー処理)を実行する。
 *
 * まず①について、呼び出しされるサーブレットは、
 *   [リモート接続先URL]servlet/remoteControl?class=TransferReadWrapper&amp;type=read になります。
 *   [リモート接続先URL]は、http://[ホスト名]:[ポート番号]/[コンテキスト名]の形式になりますが、
 *   これについては、読取対象で指定します。
 *   接続時にエラーが発生した場合や、レスポンスデータに対して"row_error"という
 *   文字列が存在する場合は、エラーとして処理します。
 *   それ以外の場合は、正常終了として処理します。
 * 次に②について、サーブレット経由で行われる伝送処理について、その読取方法は、
 *   このクラスを継承した各サブクラスのクラス名により決定されます。
 *   具体的には、サブクラスのクラス名に対して、このクラス(親クラス)のクラス名+"_" を除外した部分が
 *   読取方法として認識されます。
 *   例として、サブクラス名がTransferRead_HTTP_CB01の場合、接続先における読取方法は
 *   旧伝送DB読取(CB01)となります。
 *   また、リモートホスト上で実行される伝送処理の[リモート読取対象]は、元の読取対象で設定します。
 *   このことから、HTTP経由で読取処理を行う場合、元の読取対象には、[リモート接続先URL]と
 *   [リモート読取対象]の2つを設定する必要があります。
 *   具体的な設定方法については各サブクラスのJavaDocを参照して下さい。
 * ③について、返されるXMLデータは[レスポンスデータのXML構造]の形式になります。
 *   ここで、dataListは、伝送実行オブジェクトに渡されるデータになります。
 *   また、keyListについては、伝送実行終了後、HTTP経由で完了処理を行うための更新キーになります。
 *   (ローカルの伝送読取オブジェクトはトランザクションに対して同一オブジェクトになりますが、
 *    HTTP接続先でサーブレット経由で生成される、リモートの伝送読取オブジェクトは、
 *    読取処理と完了/エラー処理で異なります。このため、読取したデータのキーをローカルに保持し、
 *    最後の完了/エラー処理の際に、読取したデータのキー(更新キー)をPostDataとして渡すことで、
 *    正しく完了/エラー処理が行われるように対応しています)
 * 最後に④について、呼び出しされるサーブレットは、
 *   [リモート接続先URL]servlet/remoteControl?class=TransferReadWrapper&amp;type=complete (正常終了時)
 *   [リモート接続先URL]servlet/remoteControl?class=TransferReadWrapper&amp;type=error    (エラー発生時)
 *   となります。
 *
 * HTTP接続時には、以下のポストデータが送信されます。
 * [ポストデータ]
 * ・KBREAD			(読取方法) ※サブクラスの最後の"_"(アンダーバー)以降の文字列
 * ・READOBJ		(リモート読取対象) ※ローカルの読取対象からリモート接続先URLを除いた文字列
 * ・READPRM		(読取パラメーター)
 * ・KBEXEC			(実行方法)
 * ・EXECDBID		(実行接続先DBID)
 * ・EXECOBJ		(実行対象)
 * ・EXECPRM		(実行パラメーター)
 * ・ERROR_SENDTO	(読み取り元ホストコード)
 * ・HFROM (読み取り元ホストコード)
 * ・n (キー件数)
 * ・k1～kn (キー)
 *
 * また、データ読取時に返されるXMLは以下の構造をしています。
 * [レスポンスデータのXML構造]
 * &lt;root&gt;
 *  &lt;dataList&gt;
 *   &lt;data&gt;aaa&lt;/data&gt;
 *   &lt;data&gt;bbb&lt;/data&gt;
 *   &lt;data&gt;ccc&lt;/data&gt;
 *   &lt;data&gt;ddd&lt;/data&gt;
 *   &lt;data&gt;eee&lt;/data&gt;
 *  &lt;/dataList&gt;
 *  &lt;keyList&gt;
 *   &lt;key&gt;KEY1&lt;/key&gt;
 *   &lt;key&gt;KEY2&lt;/key&gt;
 *   &lt;key&gt;KEY3&lt;/key&gt;
 *  &lt;/keyList&gt;
 * &lt;/root&gt;
 *
 * @og.group 伝送システム
 *
 * @version  5.0
 * @author   Hiroki.Nakamura
 * @since    JDK1.6
 */
public abstract class TransferRead_HTTP implements TransferRead {

	// リモート制御サーブレット名
	private static final String REMOTE_SERVLET = "servlet/remoteControl?class=TransferReadWrapper";

	// 更新対象のキー
	private String[] keys ;

	/**
	 * URL接続を行いデータを読み取ります。
	 * 接続パラメータには、"type=read"が付加されます。
	 *
	 * @param config 伝送設定オブジェクト
	 * @param tran トランザクションオブジェクト
	 *
	 * @return 読み取りしたデータ(配列)
	 * @og.rtnNotNull
	 */
	@Override
	public String[] read( final TransferConfig config, final Transaction tran ) {
		splitReadObj( config.getReadObj() );
		final String url = getRemoteHost() + REMOTE_SERVLET+ "&type=read";
		final String postData = getPostData( keys, config );
		URLConnect conn = null;
		String data = null;
		try {
			conn = connect( url, postData, config );
			data = readData( conn );
		}
		catch( IOException ex ) {
			final String errMsg = "URL接続時に例外が発生しました。[URL=" + url + "]";
			throw new RuntimeException( errMsg, ex );
		}
		finally {
			if( conn != null ) { conn.disconnect(); }
		}

		final List<String> valList = new ArrayList<String>();
		final List<String> keyList = new ArrayList<String>();

		final DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
		Document doc = null;
		try {
			final DocumentBuilder builder = dbfactory.newDocumentBuilder();
			doc = builder.parse( new ByteArrayInputStream( data.getBytes( "UTF-8" ) ) );
		}
//		catch( Exception ex ){				// 6.1.0.0 (2014/12/26) refactoring
		catch( ParserConfigurationException ex ) {
			final String errMsg = "XMLパース時にエラーが発生しました。";
			throw new RuntimeException( errMsg, ex );
		}
		catch( UnsupportedEncodingException ex ) {
			final String errMsg = "バイトエンコード時にエラーが発生しました。";
			throw new RuntimeException( errMsg, ex );
		}
		catch( SAXException ex ) {
			final String errMsg = "SAXパース時にエラーが発生しました。";
			throw new RuntimeException( errMsg, ex );
		}
		catch( IOException ex ) {
			final String errMsg = "ファイル入力時にエラーが発生しました。";
			throw new RuntimeException( errMsg, ex );
		}
		final Element root = doc.getDocumentElement();

		// データ部分を取得します。
		final NodeList dataChilds = root.getElementsByTagName( "dataList" ).item(0).getChildNodes();
		final int numDataChild = dataChilds.getLength();
		for( int i=0; i<numDataChild; i++ ) {
			final Node nd = dataChilds.item(i);
			// 6.0.0.1 (2014/04/25) These nested if statements could be combined
			if( ( nd.getNodeType() == Node.ELEMENT_NODE ) && "data".equals( ((Element)nd).getTagName() ) ) {
				valList.add( nd.getTextContent() );
			}
		}

		// 以降の処理でデータを更新するためのキーを取得します。
		final NodeList keyChilds = root.getElementsByTagName( "keyList" ).item(0).getChildNodes();
		final int numKeyChild = keyChilds.getLength();
		for( int i=0; i<numKeyChild; i++ ) {
			final Node nd = keyChilds.item(i);
			// 6.0.0.1 (2014/04/25) These nested if statements could be combined
			if( nd.getNodeType() == Node.ELEMENT_NODE && "key".equals( ((Element)nd).getTagName() ) ) {
				keyList.add( nd.getTextContent() );
			}
		}
		keys = keyList.toArray( new String[keyList.size()] );

		return valList.toArray( new String[valList.size()] );
	}

	/**
	 * 読取したデータに対して完了処理を行います。
	 * 接続パラメータには、"type=complete"が付加されます。
	 *
	 * @param config 伝送設定オブジェクト
	 * @param tran トランザクションオブジェクト
	 */
	@Override
	public void complete( final TransferConfig config, final Transaction tran ) {
		splitReadObj( config.getReadObj() );
		final String url = getRemoteHost() + REMOTE_SERVLET + "&type=complete";
		final String postData = getPostData( keys, config );
		URLConnect conn = null;
		try {
			conn = connect( url, postData, config );
		}
		catch( IOException ex ) {
			final String errMsg = "URL接続時に例外が発生しました。[URL=" + url + "]";
			throw new RuntimeException( errMsg, ex );
		}
		finally {
			if( conn != null ) { conn.disconnect(); }
		}
	}

	/**
	 * 読取したデータに対してエラー処理を行います。
	 * 接続パラメータには、"type=error"が付加されます。
	 *
	 * @param config 伝送設定オブジェクト
	 * @param appInfo DB接続情報
	 */
	@Override
	public void error( final TransferConfig config, final ApplicationInfo appInfo ) {
		splitReadObj( config.getReadObj() );
		final String url = getRemoteHost() + REMOTE_SERVLET + "&type=error";
		final String postData = getPostData( keys, config );
		URLConnect conn = null;
		try {
			conn = connect( url, postData, config );
		}
		catch( IOException ex ) {
			final String errMsg = "URL接続時に例外が発生しました。[URL=" + url + "]";
			throw new RuntimeException( errMsg, ex );
		}
		finally {
			if( conn != null ) { conn.disconnect(); }
		}
	}

	/**
	 * このクラスでは、サポートされていません。
	 *
	 * @return 更新キー(配列)
	 */
	@Override
	public String[] getKeys() {
		final String errMsg = "このクラスでは、サポートされてません。";
		throw new RuntimeException( errMsg );
	}

	/**
	 * このクラスでは、サポートされてません。
	 *
	 * @param keys 更新キー配列(可変長引数)
	 */
	@Override
//	public void setKeys( final String[] keys ) {
	public void setKeys( final String... keys ) {
		final String errMsg = "このクラスでは、サポートされてません。";
		throw new RuntimeException( errMsg );
	}

	/**
	 * ローカルの読取対象を、リモート接続先の読取対象とリモート接続先URLに分解します。
	 *
	 * @param localReadObj ローカルの読取対象
	 */
	protected abstract void splitReadObj( final String localReadObj );

	/**
	 * リモート接続先URLを返します。
	 * このメソッドは、{@link #splitReadObj(String)}の後に呼び出しする必要があります。
	 *
	 * @return リモート接続先URL
	 */
	protected abstract String getRemoteHost();

	/**
	 * リモート接続先の読取対象を返します。
	 * このメソッドは、{@link #splitReadObj(String)}の後に呼び出しする必要があります。
	 *
	 * @return 接続URL
	 */
	protected abstract String getRemoteReadObj();

	/**
	 * 指定のURLに接続します。
	 *
	 * @param url 接続URL
	 * @param postData POSTするデータ
	 * @param config 伝送設定オブジェクト
	 *
	 * @return 接続オブジェクト
	 * @throws IOException なんらかのエラーが発生した場合。
	 */
	protected URLConnect connect( final String url, final String postData, final TransferConfig config ) throws IOException {
		final URLConnect conn = new URLConnect( url, TransferConfig.HTTP_AUTH_USER_PASS );
		if( config.getProxyHost() != null && config.getProxyHost().length() > 0  ) {
			conn.setProxy( config.getProxyHost(),config.getProxyPort() );
		}
		conn.setCharset( "UTF-8" );
		conn.setPostData( postData );
		conn.connect();
		return conn;
	}

	/**
	 * 指定のURLに接続しレスポンスデータを返します。
	 *
	 * @param conn URL接続オブジェクト
	 *
	 * @return レスポンスデータ
	 */
	private String readData( final URLConnect conn ) throws IOException {
		final String readData = conn.readData();
		// 返されたデータ中に"row_error"が存在する場合はエラーとして処理します。
		if( readData != null && readData.indexOf( "row_error" ) >= 0 ) {
			throw new RuntimeException( readData );
		}
		return readData;
	}

	/**
	 * 伝送設定オブジェクトをURLパラメーターに変換します。
	 *
	 * @param	keys	更新キー(配列)
	 * @param	config	伝送設定オブジェクト
	 *
	 * @return	URLパラメーター
	 * @og.rtnNotNull
	 */
	protected String getPostData( final String[] keys, final TransferConfig config ) {
		// サブクラス名から親クラス名+"_"を除いた部分を読取方法とする。
		final String kbRead = getClass().getName().replace( getClass().getSuperclass().getName() + "_", "" );

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "KBREAD="		).append( StringUtil.urlEncode( kbRead ) )
			.append( "&READOBJ="		).append( StringUtil.urlEncode( getRemoteReadObj() ) )
			.append( "&READPRM="		).append( StringUtil.urlEncode( config.getReadPrm() ) )
			.append( "&KBEXEC="		).append( StringUtil.urlEncode( config.getKbExec() ) )
			.append( "&EXECDBID="	).append( StringUtil.urlEncode( config.getExecDbid() )  )
			.append( "&EXECOBJ="		).append( StringUtil.urlEncode( config.getExecObj() ) )
			.append( "&EXECPRM="		).append( StringUtil.urlEncode( config.getExecPrm() ) )
			.append( "&ERROR_SENDTO=").append( StringUtil.urlEncode( config.getErrorSendto() ) )
			.append( "&HFROM="		).append( StringUtil.urlEncode( config.getHfrom() ) );

		if( keys != null && keys.length > 0 ) {
			buf.append( "&n=" ).append( keys.length );
			for( int i=0; i<keys.length; i++ ) {
				buf.append( "&k" ).append( i ).append( '=' )		// 6.0.2.5 (2014/10/31) char を append する。
					.append( StringUtil.urlEncode( keys[i] ) );
			}
		}
		else {
			buf.append( "&n=0" );
		}

		return buf.toString();
	}
}
