/*
 * 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 java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.util.concurrent.ConcurrentMap;							// 6.4.3.3 (2016/03/04)
import java.util.concurrent.ConcurrentHashMap;						// 6.4.3.3 (2016/03/04)

import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.system.Closer;
import org.opengion.fukurou.system.LogWriter;
import static org.opengion.fukurou.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * このクラスは、jspファイルのXSLT変換に特化した、Readerオブジェクトを作成するクラスです。
 * jspファイル に記述される、jsp:directive.include を見つけて、そのファイル属性に
 * 記述されているファイルを、インクルードします。
 * Tomcat の特性上、インクルード時のファイルは、＆等のエスケープを処理しておく
 * 必要があります。
 * エスケープの前処理は、jsp:root タグのあるなしで判定します。
 * 現時点では、 &amp;amp; , &lt; , &lt;= , &gt; , &gt;= を前処理します。
 *
 * JSP では、og:head タグで、&lt;html&gt; を出力したり、htmlend.jsp インクルードで
 * &lt;/body&gt;&lt;/html&gt; を出力していますが、フレームや、フォワードなど、整合性が
 * 取れないケースがありますので、XML処理用として、&lt;html&gt; を出力していません。
 * 変換結果を、正式な HTML ファイルとして再利用される場合は、ご注意ください。
 *
 * なお、このクラスは、マルチスレッド対応されていません。
 *
 * @og.rev 4.0.0.2 (2007/12/10) 新規追加
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class JspIncludeReader {
	// 6.4.3.3 (2016/03/04) includeファイルは、共通ファイルなので、データ量は多いが数は少ない。
	private static final ConcurrentMap<String,String> INCLUDE_FILES = new ConcurrentHashMap<>();				// 6.4.3.3 (2016/03/04)

	// 5.6.7.1 (2013/08/09) デバッグ用にincludeしたファイルを保存しておきます。
	private final StringBuilder incFiles = new StringBuilder( BUFFER_MIDDLE );

	// 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
	private String realPath	;

	// タグの属性の値のみを抜き出しています。特に、<>& を含む場合。
	// 5.2.1.0 (2010/10/01) 仮廃止
	//	private static final Pattern ptn = Pattern.compile( "=[ \t]*\"([^\"]*[<>&].[^\"]*)\"" );

	/**
	 * JSP のインクルードを考慮した、JSPファイルを、String で返します。
	 * このメソッドは、内部で再帰定義されています。つまり、jsp:directive.include
	 * 文字列が見つかった場合は、その代わりに、ファイル名を取出して、もう一度
	 * このメソッドを呼び出します。インクルードファイルとの関連をチェックする為に
	 * ダミーのspanタグを入れておきます。
	 * &lt;span type="jsp:directive" include="ファイル名"&gt;&lt;!-- --&gt;&lt;/span&gt;
	 * ただし、ソースチェック時に、
	 * Ver4 以降で、インクルードファイルに、XML宣言と、jsp:root を付与するケースがあります。
	 * 擬似的に取り込むときには、XML宣言は削除します。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) directive.include で、XMLタグとroot タグは取り込まない。
	 * @og.rev 5.2.1.0 (2010/10/01) エスケープ処理の引数を廃止します。
	 * @og.rev 5.6.5.2 (2013/06/21) 小細工内容の変更。replaceAll にするのと、スペースまたはタブを使用します。
	 * @og.rev 5.6.7.1 (2013/08/09) コメントの処理のバグ修正。includeファイル名保存。
	 * @og.rev 5.6.7.1 (2013/08/09) includeファイルが存在しない場合は、gf共有から取得する。
	 * @og.rev 5.6.7.2 (2013/08/16) includeファイルを取り込む場合、代わりのspanタグを出力しておきます。
	 * @og.rev 5.6.7.4 (2013/08/30) includeファイルの先頭のpageEncoding指定のチェック用 span タグの出力
	 * @og.rev 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
	 * @og.rev 6.3.9.1 (2015/11/27) htmlend.jsp をインクルード処理しない箇所で、判定方法を、htmlend を含むかどうかに変更。
	 * @og.rev 6.4.3.2 (2016/02/19) /jsp/*** で始まるファイルのみ、キャッシュします。
	 *
	 * @param	file	JSPファイル
	 * @param	encode	ファイルのエンコード
	 *
	 * @return	インクルードを考慮した、JSPファイル
	 * @og.rtnNotNull
	 */
	public String getString( final File file,final String encode ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE ) ;
		final BufferedReader reader = FileUtil.getBufferedReader( file,encode );

		// ファイルが、jsp 直下かどうかを判断します。
		final String parentFile = file.getParent() ;
		final boolean isUnder = parentFile.endsWith( "\\jsp" );

		int  cmntIn    = -1;
		int  cmntOut   = -1;
		boolean isCmnt = false;
		boolean isEscape = true;	// エスケープするかどうか(true:する/false:しない)
		try {
			String line ;
			while((line = reader.readLine()) != null) {
				// 5.2.1.0 (2010/10/01) directive.include で、XMLタグは取り込まない。
				if( line.indexOf( "<?xml" ) >= 0 && line.indexOf( "?>" ) >= 0 ) { continue; }
				// jsp:root があれば、エスケープ処理を行わない
				if( line.indexOf( "<jsp:root" ) >= 0 ) { isEscape = false; }

				// コメントの削除
				cmntIn  = line.indexOf( "<!--" );
				cmntOut = line.indexOf( "-->" );
				if( cmntIn >= 0 && cmntOut >= 0 ) {
					line = line.substring( 0,cmntIn ) + line.substring( cmntOut+3 );	// 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
				}
				else if( cmntIn >= 0 && cmntOut < 0 ) {
					line = line.substring( 0,cmntIn );
					isCmnt = true;
				}
				else if( cmntIn < 0  && cmntOut >= 0 ) {
					line = line.substring( cmntOut+3 );			// 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
					isCmnt = false;
				}
				else if( isCmnt && cmntIn < 0 && cmntOut < 0 ) { continue; }

				// 特殊処理：og:head で html タグを出力している。
	//			if( line.indexOf( "<og:head" ) >= 0 ) {
	//				buf.append( "<html>" );
	//			}

				if( isEscape ) {
					// 5.6.5.2 (2013/06/21) 小細工内容の変更。replaceAll にするのと、スペースまたはタブを使用します。
					// & , < , <= , > , >= を前処理します。
					line = line.replaceAll( "&"  ,"&amp;" );				// ちょっと小細工
					line = line.replaceAll( "[ \\t]<[ \\t]"," &lt; " );		// ちょっと小細工
					line = line.replaceAll( "[ \\t]>[ \\t]"," &gt; " );		// ちょっと小細工
					line = line.replaceAll( "[ \\t]<="," &lt;=" );			// ちょっと小細工
					line = line.replaceAll( "[ \\t]>="," &gt;=" );			// ちょっと小細工
	// 5.2.1.0 (2010/10/01) 仮廃止
	//				Matcher mtch = ptn.matcher( line );
	//				int adrs = 0;
	//				StringBuilder buf2 = new StringBuilder();
	//				while( mtch.find(adrs) ) {
	//					String grp = mtch.group(1);
	//					String htm = StringUtil.htmlFilter( grp );
	//					int in = mtch.start(1);
	//					buf2.append( line.substring( adrs,in ) ).append( htm );
	//					adrs = mtch.end(1);
	//				}
	//				buf2.append( line.substring( adrs ) );
	//				line = buf2.toString();
				}

				final int st = line.indexOf( "<jsp:directive.include" );
				if( st < 0 ) { buf.append( line ); }	// include が無ければ、そのまま追加
				else {
					buf.append( line.substring( 0,st ) );
					final int fin = line.indexOf( '\"',st );			// ファイルの最初
					final int fout= line.indexOf( '\"',fin+1 );			// ファイルの最後
					final String fname = line.substring( fin+1,fout );	// ファイル名

					// 5.6.7.2 (2013/08/16) includeファイルを取り込む場合、代わりのspanタグを出力しておきます。
					buf.append( "<span type=\"jsp:directive\" include=\"" )
						.append( fname ).append( "\" ><!-- --></span>" ) ;

					// htmlend.jsp の インクルードは行わない。
	//				if( fname.endsWith( "htmlend.jsp" ) ) {
					if( fname.contains( "htmlend" ) ) {					// 6.3.9.1 (2015/11/27)
						if( buf.indexOf( "<body" ) >= 0 && buf.indexOf( "</body>" ) < 0 ) {
							buf.append( "</body>" );
						}

	//					if( buf.indexOf( "<html" ) >= 0 ) {
	//						buf.append( "</html>" );
	//					}
					}
					else {
						// 5.6.7.1 (2013/08/09) デバッグ用にincludeしたファイルを保存しておきます。
						if( incFiles.length() > 0 ) { incFiles.append( " , " ); }
						incFiles.append( fname );

						// 5.6.7.1 (2013/08/09) includeしたファイルをキャッシュから検索します。
						String fileData = INCLUDE_FILES.get( fname );	// キャッシュを検索(fname がキー)
						if( fileData == null ) {
							// ちょっと小細工
							String fname2 = fname ;
							// include するファイルは、/jsp/ からの絶対パス。
							// jsp 直下の場合は、./ 、それ以外は、../ と置き換えます。
							if( isUnder ) { fname2 = fname2.replace( "/jsp/","./" ); }
							else 		  { fname2 = fname2.replace( "/jsp/","../" ); }
							// 5.6.7.1 (2013/08/09) includeファイルが存在しない場合は、gf共有から取得する。(jar圧縮対応)
							File newfile = new File( parentFile,fname2 );
							if( !newfile.exists() ) {
								if( fname2.contains( "/common/" ) || fname2.contains( "/menu/" ) ) {
									if( realPath == null ) {
										// 本当は classPathから、取得すべき。
										// 今は、実行環境の相対パスの位置に、gf/jsp/common,menu のファイルが必要。
										fname2 = isUnder
													?	"./../../gf/jsp/"  + fname2.substring( 2 )
													:	"../../../gf/jsp/" + fname2.substring( 3 ) ;
										newfile = new File( parentFile,fname2 );		// ここでなければ、エラーになる。
									}
									else {
										// 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
										newfile = new File( realPath,fname );	// 稼働している gf の common 等を使用します。
									}
								}
							}
							fileData = getString( newfile,encode );

						 	// 5.6.7.4 (2013/08/30) includeファイルの先頭のpageEncoding指定のチェック用 span タグの出力
							// インクルードファイルの先頭には、pageEncoding="UTF-8" 宣言が必要(UTF-8かどうかは未チェック)
							if( ! fileData.startsWith( "<jsp:directive.page pageEncoding" ) ) {
								// チェック用のspanタグを出力しておきます。
								buf.append( "<span type=\"jsp:directive\" pageEncoding=\"non\" file=\"" )
									.append( fname ).append( "\" ><!-- --></span>" ) ;
							}
							// 6.4.3.2 (2016/02/19) /jsp/*** で始まるファイルのみ、キャッシュします。
							// 5.6.7.1 (2013/08/09) includeしたファイルをキャッシュしておきます。
							if( fname.startsWith(  "/jsp/" ) ) {
								INCLUDE_FILES.put( fname,fileData );			// includeファイルをキャッシュ(fname がキー)
							}
						}
						buf.append( fileData );
					}
					final int tagout = line.indexOf( "/>",fout+1 );			// タグの最後 XML なので、このまま。

					buf.append( line.substring( tagout+2 ) );
				}

				// og:commonForward を見つけた場合は、最後に html タグを出力する。
	//			if( line.indexOf( "<og:commonForward" ) >= 0 ) {
	//				buf.append( "</html>" );
	//			}

				buf.append( CR );
			}
		}
		catch( final IOException ex ) {
			LogWriter.log( ex );
		}
		finally {
			Closer.ioClose( reader );
		}
		return buf.toString();
	}

	/**
	 * jspInclude=true 時に、/jsp/common/** 等の include ファイルが存在しない場合の共有取得場所を指定します。
	 *
	 * 引数の処理対象ファイル(transformの引数ファイル)が、『.jsp』で、かつ、jspInclude=true の場合、
	 * そのファイルを INCLUDE するのですが、/jsp/common/** 等の include ファイルは、
	 * エンジン共通として、jspCommon6.x.x.x.jar で提供しています。
	 * 従来は、処理対象jspの相対パスで、../../../gf/jsp/commom/** を取り込んでいましたが、
	 * Tomcat起動フォルダ以外のシステムのJSPチェックなどを行う場合は、gf フォルダが存在しない
	 * ケースがあります。
	 * そこで、確実にgf が存在する、処理をキックしている環境の gf を使用するように変更します。
	 * その環境とは、つまり、エンジン内部変数の REAL_PATH ですが、jsp などが実行していないと取得できません。
	 *
	 * @param	path	/jsp/common/** 等の include ファイルの共有取得場所
	 */
	public void setRealPath( final String path ) {
		realPath = path ;
	}

	/**
	 * インクルードしたファイル名(相対パス)のリスト文字列を返します。
	 * 通常は、XSLT変換処理でエラーが発生した場合は、includeファイルの整合性が
	 * おかしい場合が多いので、デバッグ情報として利用します。
	 * ただし、エラー発生時の位置特定まではできません。
	 *
	 * この内部変数は、インスタンス変数ですので、includeファイルのキャッシュとは寿命が異なります。
	 *
	 * @og.rev 5.6.7.1 (2013/08/09) 新規追加
	 *
	 * @return includeファイル名のリスト文字列
	 * @og.rtnNotNull
	 */
	public String getIncludeFiles() {
		return incFiles.toString();
	}

	/**
	 * インクルードしたファイルのキャッシュをクリアします。
	 * キャッシュは、インスタンスではなく、スタティック変数で管理しています。
	 * よって、一連の処理の初めと最後にクリアしておいてください。
	 *
	 * @og.rev 5.6.7.1 (2013/08/09) 新規追加
	 */
	public static void cacheClear() {
		INCLUDE_FILES.clear();
	}

	/**
	 * テスト用の main メソッド。
	 *
	 * Usage: org.opengion.fukurou.xml.JspIncludeReader inFile [outFile]
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		final JspIncludeReader reader = new JspIncludeReader();
		final String xml = reader.getString( new File( args[0] ),"UTF-8" );

		if( args.length > 1 ) {
			final java.io.PrintWriter writer = FileUtil.getPrintWriter( new File( args[1] ),"UTF-8" );
			writer.print( xml );
			Closer.ioClose( writer );
		}
		else {
			System.out.println( xml );
		}
	}
}
