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

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLConnection;

/**
 * SOAPConnect は、URLConnectクラスの拡張版で、SOAP接続を行うための機能を追加しています。 
 * 基本的な機能は、{@link org.opengion.fukurou.util.URLConnect}を参照して下さい。
 * 
 * SOAP対応の追加機能としては、以下の2点があります。
 * 
 * ①ヘッダー情報の変更
 *  SOAP接続では、通常のURLConnectに加えて、以下のヘッダー情報を付加しています。
 * 
 *  1.Content    = text/xml;charset=UTF-8"
 *  2.Accept     = text/xml, multipart/related, text/html, image/gif, image/jpeg,
 *  3.Soapaction = "[NameSpace][SOAPMethodName]"
 * 
 * ②SOAPメッセージの生成機能
 *  SOAPメッセージは、以下のようなXML構造で定義されます。
 *  
 *  <env:Envelope env:xmlns="..." xsi:xmlns="..."
 *    <env:Body>
 *       <tns:[methodName] xmlns:tns="[nameSpace]">
 *          [methodParameter(XML)]
 *        </tns:[methodName]>
 *    </env:Body>
 *  </env:Envelope>
 *  
 *  SOAPConnectクラスでは、[methodParameter(メソッドパラメーター)]の定義方法に2種類の方法があります。
 *
 *  (a)keys,valsによる指定
 *   keys,valsを指定することで、これらを内部的にXMLデータに変換し、パラメーター部分のXML
 *   情報を生成します。
 *   
 *   例)
 *     keys="param0>AAA,param0>BBB,param1>CCC,DDD"
 *     vals="v1,v2,v3,v4"
 *    [変換結果]
 *       <param0>
 *         <AAA>v1</AAA>
 *         <BBB>v2</BBB>
 *       </param0>
 *       <param1>
 *         <CCC>v3</CCC>
 *       </param1>
 *       <DDD>v4</DDD>
 *
 *    この定義方法では、項目の値を"null"とすることで、XMLで言うところの
 *    「xsi:nil=\"true\"」のデータを表現することもできます。
 *
 *    また、キー名の先頭を'@'にすることで、項目名に名前空間のPREFIXを付加することができます。
 *    一般的には、JavaやRubyで実装されたWebサービスを呼び出しする場合は、必要ありませんが、
 *    .NETで実装されたWebサービスを呼び出しする場合は、各項目にPREFIXを付与しないと、正しく
 *    パラメーターを渡すことができません。
 *
 *    ※現時点では、keysの階層定義は、2階層まで対応しています。
 *      3階層以上のXML構造を定義する場合は、postFile属性によるファイル指定又は、Body部分で直接
 *      XMLデータを記述して下さい。
 *
 *  (b)XMLデータを直接指定
 *    メソッドパラメーターの部分のXMLデータを直接指定します。
 *    この場合、(a)のように、xsi:nil=\"true\"や、パラメーターキーへのPREFIXの付加は、予め行って
 *    おく必要があります。
 *    なお、パラメーターキーのPREFIXは、"tns:"です。
 *
 * java org.opengion.fukurou.util.SOAPConnect [<-info/-data>] <url> [user:passwd]
 *
 *   args[*] : [<-info/-data>]      情報の取得か、データの取得かを指定します(初期値:-data)。
 *   args[*] : [<-data=ファイル名>] メソッドのパラメーターが記述されたXMLファイルを指定します。。"					);
 *   args[*] : [<-out=ファイル名>]  結果をファイルに出力します。ファイルエンコードも指定します。"				);
 *   args[*] : [<-nameSpace=名前空間>]    メソッド名及びパラメーターの名前空間を指定します。"					);
 *   args[*] : [<-methodName=メソッド名>] メソッド名を指定します。"											);
 *   args[*] : [<-keys=キー一覧>]   メソッドのパラメーターのキー一覧を指定します。(dataを指定した場合は無視されます)" );
 *   args[*] : [<-vals=値一覧>]     メソッドのパラメーターの値一覧を指定します。  (dataを指定した場合は無視されます)" );
 *   args[A] : <url>                ＵＲＬを指定します。GETの場合、パラメータは ?KEY=VALです。
 *   args[B] : [<user:passwd>]      BASIC認証のエリアへのアクセス時に指定します。
 *
 * @version  4.0
 * @author   Hiroki Nakamura
 * @since    JDK5.0,
 */
public class SOAPConnect extends URLConnect {

	private static final String	CONTENT_TYPE	= "text/xml;charset=UTF-8";
	private static final String	ACCEPT			= "text/xml, multipart/related, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2";

	final private String nameSpace;
	final private String methodName;

	/**
	 * コンストラクター
	 * 
	 * ここでは、送信するXMLデータをキー及び値の一覧から生成するためのコンストラクターを定義しています。
	 *
	 * @param url    String 接続するアドレスを指定します。(http://server:port/dir/file.html)
	 * @param pass   String ユーザー：パスワード(認証接続が必要な場合)
	 * @param ns     String 名前空間を指定します。
	 * @param method String メソッド名を指定します。
	 * @param ks     String 送信するメソッドパラメーターのキー一覧を指定します。
	 * @param vs     String 送信するパラメーターの値一覧を指定します。
	 */
	public SOAPConnect( final String url, final String pass, final String ns, final String method, final String[] ks, final String[] vs ) {
		this( url, pass, ns, method, makeParamData( ks, vs ) );
	}

	/**
	 * コンストラクター
	 * 
	 * ここでは、送信するXMLデータを直接指定するためのコンストラクターを定義しています。
	 *
	 * @param url    String 接続するアドレスを指定します。(http://server:port/dir/file.html)
	 * @param pass   String ユーザー：パスワード(認証接続が必要な場合)
	 * @param ns     String 名前空間を指定します。
	 * @param method String メソッド名を指定します。
	 * @param xmlData String パラメーターのXMLデータ
	 */
	public SOAPConnect( final String url, final String pass, final String ns, final String method, final String xmlData ) {
		super( url, pass );
		nameSpace = ns;
		methodName = method;
		setPostData( getSoapEnvelop( xmlData ) );
	}

	/**
	 * URL と ユーザー：パスワードを与えて、URLConnectionを返します。
	 *
	 * SOAP接続では、通常のURLConnectに加えて、以下のヘッダー情報を付加しています。
	 * 
	 * 1.Content    = text/xml;charset=UTF-8"
	 * 2.Accept     = text/xml, multipart/related, text/html, image/gif, image/jpeg,
	 * 3.Soapaction = "[NameSpace][SOAPMethodName]"
	 * 
	 * @return  URLConnection オブジェクト
	 * @throws  IOException
	 * @see URLConnect#getConnection()
	 */
	@Override
	protected URLConnection getConnection() throws IOException {
		URLConnection urlConn = super.getConnection();
		urlConn.setRequestProperty( "Content-Type", CONTENT_TYPE );
		urlConn.setRequestProperty( "Accept", ACCEPT );
		urlConn.setRequestProperty( "Soapaction", "\"" + nameSpace + methodName + "\"" );
		return urlConn;
	}

	/**
	 * メソッドパラメーターのXMLデータに、Envelop情報を付加し、SOAP通信を行うための
	 * 完全なXML情報を生成します。
	 * 
	 * @param data メソッドのパラメーターとなるXMLデータ
	 * @return SOAPで送信される全体のXML情報
	 */
	private String getSoapEnvelop( final String data ) {
		StringBuilder buf = new StringBuilder();
		
		buf.append( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" );
		buf.append( "<env:Envelope " );
		buf.append( " xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\" " );
		buf.append( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><env:Body>" );
		buf.append( "<tns:" ).append( methodName ).append( " xmlns:tns=\"" ).append( nameSpace ).append( "\">" );
		buf.append( data );
		buf.append( "</tns:" ).append( methodName ).append( ">" );
		buf.append( "</env:Body></env:Envelope>" );

		return buf.toString();
	}

	/**
	 * キー一覧と値一覧から、メソッドパラメータのXMLデータを生成します。
	 * 
	 * @param keys キー一覧
	 * @param vals 値一覧
	 * @return メソッドパラメーターのXMLデータ
	 */
	private static final String makeParamData( final String[] keys, final String[] vals ) {
		StringBuilder buf = new StringBuilder();

		String preParentKey = null;
		String key = null;
		for( int i = 0; i < keys.length; i++ ) {
			String[] keyTags = StringUtil.csv2Array( keys[i], '>' );

			if( keyTags.length > 2 ) {
				String errMsg = "keys,vals形式では2階層までのデータのみを定義することができます。"
							+ "3階層以上のデータに関しては、直接XMLでメソッドパラメーターを指定して下さい。";
				throw new RuntimeException( errMsg );
			}

			// 親の終了タグを出力
			if( preParentKey != null
					&& ( keyTags.length == 1 || !preParentKey.equals( keyTags[0] ) ) ) {
				buf.append( getKeyTag( preParentKey, true ) );
			}

			if( keyTags.length > 1 ) {
				// 親の開始タグを出力
				if( preParentKey == null || !preParentKey.equals( keyTags[0] ) ) {
					buf.append( getKeyTag( keyTags[0], false ) );
				}
				key = keyTags[1];
				preParentKey = keyTags[0];
			}
			else {
				key = keyTags[0];
				preParentKey = null;
			}

			// 値の出力
			if( "null".equals( vals[i] ) ) {
				buf.append( getKeyTag( key, false, "xsi:nil=\"true\"" ) );
			}
			else {
				buf.append( getKeyTag( key, false ) ).append( vals[i] );
			}
			buf.append( getKeyTag( key, true ) );
		}

		// 親の終了タグの出力
		if( preParentKey != null ) {
			buf.append( getKeyTag( preParentKey, true ) );
		}

		return buf.toString();
	}

	/**
	 * タグキーからタグ文字列を生成します。
	 * 
	 * @param key タグキー
	 * @param isEnd 終了タグ or 開始タグ
	 * @return タグ文字列
	 */
	private static final String getKeyTag( final String key, final boolean isEnd ) {
		return getKeyTag( key, isEnd, null );
	}

	/**
	 * タグキーからタグ文字列を生成します。
	 * 
	 * @param key タグキー
	 * @param isEnd 終了タグ or 開始タグ
	 * @param attr 属性値
	 * @return タグ文字列
	 */
	private static final String getKeyTag( final String key, final boolean isEnd, final String attr ) {
		StringBuilder buf = new StringBuilder();
		buf.append( '<' );
		if( isEnd ) { buf.append( '/' ); }

		boolean isQualified = ( key.charAt( 0 ) == '@' );
		String tmp = ( isQualified ? key.substring( 1 ) : key );
		if( isQualified ) {
			buf.append( "tns:" );
		}
		buf.append( tmp );
		if( attr != null && attr.length() > 0 ) {
			buf.append( " " ).append( attr );
		}
		buf.append( '>' );
		return buf.toString();
	}

	/**
	 * サンプル実行用のメインメソッド
	 *
	 * java org.opengion.fukurou.util.SOAPConnect [<-info/-data>] <url> [user:passwd]
	 *
	 *   args[*] : [<-info/-data>]      情報の取得か、データの取得かを指定します(初期値:-data)。
	 *   args[*] : [<-data=ファイル名>] 送信するデータが記述されたXMLファイルを指定します。。"					);
	 *   args[*] : [<-out=ファイル名>]  結果をファイルに出力します。ファイルエンコードも指定します。"				);
	 *   args[*] : [<-nameSpace=名前空間>]    メソッド名及びパラメーターの名前空間を指定します。"					);
	 *   args[*] : [<-methodName=メソッド名>] メソッド名を指定します。"											);
	 *   args[*] : [<-keys=キー一覧>]   メソッドのパラメーターのキー一覧を指定します。(dataを指定した場合は無視されます)" );
	 *   args[*] : [<-vals=値一覧>]     メソッドのパラメーターの値一覧を指定します。  (dataを指定した場合は無視されます)" );
	 *   args[A] : <url>                ＵＲＬを指定します。GETの場合、パラメータは ?KEY=VALです。
	 *   args[B] : [<user:passwd>]      BASIC認証のエリアへのアクセス時に指定します。
	 *
	 * @param  args String[]
	 */
	public static void main( final String[] args ) throws IOException {
		if( args.length < 3 ) {
			LogWriter.log( "Usage: java org.opengion.fukurou.util.SOAPConnect [<-info/-data>] <url> [user:passwd]"						);
			LogWriter.log( "   args[*] : [<-info/-data>]      情報の取得か、データの取得かを指定します(初期値:-data)。"					);
			LogWriter.log( "   args[*] : [<-data=ファイル名>] 送信するデータが記述されたXMLファイルを指定します。。"					);
			LogWriter.log( "   args[*] : [<-out=ファイル名>]  結果をファイルに出力します。ファイルエンコードも指定します。"				);
			LogWriter.log( "   args[*] : [<-nameSpace=名前空間>]    メソッド名及びパラメーターの名前空間を指定します。"					);
			LogWriter.log( "   args[*] : [<-methodName=メソッド名>] メソッド名を指定します。"											);
			LogWriter.log( "   args[*] : [<-keys=キー一覧>]   メソッドのパラメーターのキー一覧を指定します。(dataを指定した場合は無視されます)" );
			LogWriter.log( "   args[*] : [<-vals=値一覧>]     メソッドのパラメーターの値一覧を指定します。  (dataを指定した場合は無視されます)" );
			LogWriter.log( "   args[A] : <url>                ＵＲＬを指定します。GETの場合、パラメータは ?KEY=VALです。"				);
			LogWriter.log( "   args[B] : [<user:passwd>]      BASIC認証のエリアへのアクセス時に指定します。"							);
			return;
		}

		boolean isInfo	= false ;
		String postFile	= null ;
		String outFile	= null ;
		String ns		= null;
		String method	= null;
		String[] ks		= null;
		String[] vs		= null;
		String[] vals	= new String[2];		// url,userPass の順に引数設定

		int adrs = 0;
		for( int i=0; i<args.length; i++ ) {
			String arg = args[i];
			if( arg.equalsIgnoreCase( "-info" ) ) {
				isInfo = true;
			}
			else if( arg.equalsIgnoreCase( "-data" ) ) {
				isInfo = false;
			}
			else if( arg.startsWith( "-post=" ) ) {
				postFile = arg.substring( 6 );
			}
			else if( arg.startsWith( "-out=" ) ) {
				outFile = arg.substring( 5 );
			}
			else if( arg.startsWith( "-nameSpace=" ) ) {
				ns = arg.substring( 11 );
			}
			else if( arg.startsWith( "-methodName=" ) ) {
				method = arg.substring( 12 );
			}
			else if( arg.startsWith( "-keys=" ) ) {
				ks = StringUtil.csv2Array( arg.substring( 6 ) );
			}
			else if( arg.startsWith( "-vals=" ) ) {
				vs = StringUtil.csv2Array( arg.substring( 6 ) );
			}
			else if( arg.startsWith( "-" ) ) {
				System.out.println( "Error Argment:" + arg );
			}
			else {
				vals[adrs++] = arg;
			}
		}

		String urlStr	= vals[0] ;
		String userPass = vals[1] ;


		// POST データは、connect() する前に、設定します。
		URLConnect conn = null;
		if( postFile != null ) {
			FileString file = new FileString();
			file.setFilename( postFile );
			String postData = file.getValue();
			conn = new SOAPConnect( urlStr,userPass, ns, method, postData );
		}
		else {
			conn = new SOAPConnect( urlStr,userPass, ns, method, ks, vs );
		}

		conn.connect();

		final PrintWriter writer ;
		if( outFile != null ) {
			writer = FileUtil.getPrintWriter( new File( outFile ),"UTF-8" );
		}
		else {
			writer = FileUtil.getLogWriter( "System.out" );
		}

		if( isInfo ) {
			writer.println( "URL    :" + conn.getUrl() );
			writer.println( "Type   :" + conn.getType() );
			writer.println( "Code   :" + conn.getCode() );
			writer.println( "Message:" + conn.getMessage() );
			writer.println( "Charset:" + conn.getCharset() );
		}
		else {
			writer.println( conn.readData() );
		}

		conn.disconnect();

		Closer.ioClose( writer );
	}
}
