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

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.util.QrcodeImage;
import org.opengion.fukurou.util.ReplaceString;
import static org.opengion.fukurou.system.HybsConst.CR ;				// 6.1.0.0 (2014/12/26)
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import java.io.IOException;
import java.util.concurrent.ConcurrentMap;							// 6.4.3.3 (2016/03/04)
import java.util.concurrent.ConcurrentHashMap;						// 6.4.3.1 (2016/02/12) refactoring
import java.util.regex.Pattern;
import java.util.regex.Matcher ;

/**
 * DBTableReport インターフェース を実装したHTMLをパースするクラスです。
 * AbstractDBTableReport を継承していますので，writeReport() のみオーバーライドして，
 * 固定長文字ファイルの出力機能を実現しています。
 *
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class DBTableReport_HTML extends AbstractDBTableReport {
	private static final String TR_IN        = "<tr" ;
	private static final String TR_OUT       = "</tr>" ;
	private static final String TD_OUT       = "</td>" ;			// 3.5.5.9 (2004/06/07)
	private static final String PAGE_BREAK   = "page-break" ;
	private static final String PAGE_END_CUT = "PAGE_END_CUT" ;		// 3.6.0.0 (2004/09/17)
	private static final String END_TAG      = "</table></body></html>";
	private static final String CUT_TAG1     = "<span";
	private static final String CUT_TAG2     = "</span>";
	private static final String SPACE_ST     = "<span style=\"mso-spacerun: yes\">";	// 3.6.0.0 (2004/09/17)
	private static final String SPACE        = "&nbsp;";								// 3.6.0.0 (2004/09/17)
	private static final String SPACE_ED     = " </span>";							// 3.6.0.0 (2004/09/17)
	private static final String FRAMESET     = "Excel Workbook Frameset" ;

	// <td xxx="yyy">zzzz</td> 形式とマッチし、>zzzz< 部分を前方参照します。
	private static final Pattern PTN1 = Pattern.compile("<td[^>]*(>.*?<)/td>");
	// >aaaa<span bb="cc">dddd</span>eeee< 形式に２文字以上のスペースを含むデータと
	// マッチし、aaaa,dddd,eeee を前方参照します。
	private static final Pattern PTN2 = Pattern.compile("[^>]*>([^<]*?  ++[^<]*?)<");
	// aa   bb    cc    形式とマッチし、各連続スペース部分を前方参照します。
	private static final Pattern PTN3 = Pattern.compile("(  +)");

	private boolean  fileEnd		;		// ファイルの読み取り制御

	// 3.6.1.0 (2005/01/05) QRコード(２次元バーコード)用の出力ファイル管理
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private ConcurrentMap<String,String> qrFileMap	;
	// <v:shape ･･･ alt="{@QRCODE.XXXX}" ･･･>
	//   <v:imagedata src="yyy" ･･･>･･･</v:shape>形式とマッチし、
	// xxx 部分と、yyy 部分を前方参照します。
	private static final Pattern IMGPTN1 = Pattern.compile("<v:shape [^>]*alt=\"\\{@QRCODE.([^\\}]*)\\}\"[^>]*>[^<]*<v:imagedata [^>]*src=\"([^\"]*)\"[^>]*>");
	// <img ･･･ src="yyy" ･･･ alt="{@QRCODE.XXXX}" ･･･ > 形式とマッチし、
	// yyy 部分と、xxx 部分を前方参照します。
	private static final Pattern IMGPTN2 = Pattern.compile("<img [^>]*src=\"([^\"]*)\"[^>]*alt=\"\\{@QRCODE.([^\\}]*)\\}\"[^>]*>");

	// 4.0.0 (2007/06/08) pageEndCut = true  時の LINE_COPY 機能の実装
	private static final String LINE_COPY = "LINE_COPY" ;		// 4.0.0 (2007/06/08)
	private String lineCopy	;

	// 5.7.1.0 (2013/12/06) trueの場合 PAGE_END_CUTの判定にdataOver フラグを使用。falseの場合は、rowOver を使用。
	private final boolean USE_DATAOVER = HybsSystem.sysBool( "COMPATIBLE_PAGE_END_CUT_RETRIEVAL" );

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public DBTableReport_HTML() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * 入力文字列 を読み取って、出力します。
	 * tr タグを目印に、１行(trタグ間)ずつ取り出します。
	 * 読み取りを終了する場合は、null を返します。
	 * 各サブクラスで実装してください。
	 *
	 * @og.rev 3.0.0.1 (2003/02/14) 一度もValueセットしていないのに次ページ要求があった場合は、フォーマットがおかしい
	 * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、親クラスに移動します。
	 *
	 * @return	出力文字列
	 */
	@Override
	protected String readLine() {
		if( fileEnd ) { return null; }

		// pageEndCut 時に、データがオーバーしていない間のみ、lineCopy があれば返す。
		if( pageEndCut && !rowOver && lineCopy != null ) {
			lineCopyCnt ++ ;	// 雛形は、_0 のみが毎回返される為の、加算
			return lineCopy ;
		}

		final StringBuilder buf ;
		try {
			String line = reader.readLine();
			if( line == null ) {
				if( rowOver ) {
					return null;
				}
				else {
					initReader();
					initWriter();
					line = reader.readLine();
					if( line == null ) { return null; }
				}
			}
			if( line.indexOf( FRAMESET )  >= 0 ) {
				final String errMsg = "HTML ファイルエラー :" + line + CR
								+ "Excelファイル形式がフレームになっています。(複数シートには未対応)" ;
				throw new HybsSystemException( errMsg );
			}
			if( line.indexOf( TR_IN )  >= 0 ) {
				buf = new StringBuilder( BUFFER_MIDDLE );
				buf.append( line );
				int trLevel = 1;			// 行を表す <tr> のレベル
				while( trLevel != 0 ) {
					line = reader.readLine();
					// 4.0.0 (2005/08/31) null 参照はずし対応
					// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
					if( line == null ) {
						final String errMsg = "HTML ファイルエラー :" + buf.toString() + CR
									+ "行(TR)の整合性が取れる前に、ファイルが終了しました。" ;
						throw new HybsSystemException( errMsg );
					}
					if( line.indexOf( TR_IN  ) >= 0 ) { trLevel++ ; }
					if( line.indexOf( TR_OUT ) >= 0 ) { trLevel-- ; }
					buf.append( CR ).append( line );

				}
			}
			else {
				return line;
			}
		} catch( final IOException ex ) {
			final String errMsg = "HTML ファイル 読取時にエラーが発生しました。" + reader;
			throw new HybsSystemException( errMsg,ex );		// 3.5.5.4 (2004/04/15) 引数の並び順変更
		}

		String rtnLine = buf.toString() ;

		// lineCopy 情報の取得。
		if( pageEndCut && !rowOver ) {
			// LINE_COPY は削除しますので、表示上見えるようにしておいてください。
			final int adrs = rtnLine.indexOf( LINE_COPY );
			if( adrs >= 0 ) {
				lineCopy = rtnLine.substring( 0,adrs )
							+ rtnLine.substring( adrs + LINE_COPY.length() ) ;
				rtnLine = lineCopy ;
			}
		}

		return rtnLine ;
	}

	/**
	 * 入力文字列 を加工して、出力します。
	 * {&#064;XXXX} をテーブルモデルより読み取り、値をセットします。
	 * 各サブクラスで実装してください。
	 *
	 * @og.rev 3.0.0.1 (2003/02/14) 一度もValueセットしていないのに次ページ要求があった場合は、フォーマットがおかしい
	 * @og.rev 3.0.0.2 (2003/02/20) {&#064;XXXX}文字が、EXCELに表示しきれない場合に挿入されるタグの削除処理の変更。
	 * @og.rev 3.5.0.0 (2003/09/17) {&#064;XXXX}文字のスペースを、&amp;nbsp;と置き換えます。
	 * @og.rev 3.5.0.0 (2003/09/17) {&#064;XXXX}文字がアンバランス時にHybsSystemExceptionを発行する。
	 * @og.rev 3.5.5.9 (2004/06/07) {&#064;XXXX}の連続処理のアドレス計算方法が、間違っていましたので修正します。
	 * @og.rev 3.6.0.0 (2004/09/17) pageEndCut が true の場合は、PAGE_END_CUT 文字列のある行を削除します。
	 * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、親クラスに移動します。
	 * @og.rev 3.6.1.0 (2005/01/05) QRコード(２次元バーコード)の機能追加
	 * @og.rev 3.8.1.2 (2005/12/19) PAGE_END_CUTの判定にdataOver フラグを使用。
	 * @og.rev 5.7.1.0 (2013/12/06) USE_DATAOVER が trueの場合 PAGE_END_CUTの判定にdataOver フラグを使用。falseの場合は、rowOver を使用
	 *
	 * @param	inLine	入力文字列
	 *
	 * @return	出力文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String changeData( final String inLine ) {
		// rowOver で、かつ ページブレークかページエンドカットの場合、処理終了。
		if( rowOver && inLine.indexOf( PAGE_BREAK ) >= 0 ) {
			fileEnd = true;
			return END_TAG;
		}

		String chLine = changeHeaderFooterData( inLine ) ;

		// 3.6.1.0 (2005/01/05) QRコード(２次元バーコード)の機能追加
		if( chLine.indexOf( "{@QRCODE." ) >= 0 ) {
			chLine = qrcodeReplace( chLine );
		}

		int st = chLine.indexOf( "{@" );
		// 3.8.1.2 (2005/12/19) {@XXXX}の存在しない行も PAGE_END_CUTの判定を行う。

		final StringBuilder buf = new StringBuilder( chLine );

		boolean spaceInFlag = false;	// {@XXXX} 変数のデータにスペースを含むかどうかチェック
		while( st >= 0 ) {
			int end = buf.indexOf( "}",st+2 );

			// EXCELに表示しきれない文字は、CUT_TAG1,CUT_TAG2 が挿入されてしまう為、
			// 削除する必要がある。
			final int cutSt1 = buf.indexOf( CUT_TAG1,st+2 );
			if( cutSt1 >= 0 && cutSt1 < end ) {
				final int cutEnd1 = buf.indexOf( ">",cutSt1 );

				final int cutSt2 = buf.indexOf( CUT_TAG2,end );
				if( cutSt2 >= 0 ) {
					buf.delete( cutSt2, cutSt2 + CUT_TAG2.length() );
				}
				buf.delete( cutSt1, cutEnd1+1 );
				// 途中をカットした為、もう一度計算しなおし。
				end = buf.indexOf( "}",st+2 );		// 3.5.5.9 (2004/06/07)
			}

			// 3.5.5.9 (2004/06/07)
			// 関数等を使用すると、{@XXXX} 文字列を直接加工したデータが出力される。
			// この加工されたデータは、HTML 表示に使用されるだけのため、削除します。
			// 削除方法は、{@XXX</td> を想定している為、 {@ から </td> の間です。
			final int td_out = buf.indexOf( TD_OUT,st+2 );
			if( td_out >= 0 && td_out < end ) {
				buf.delete( st, td_out );
				// {@XXXX} パラメータが消えたので、次の計算を行います。
				st = buf.indexOf( "{@",st+4 );		// 3.5.5.9 (2004/06/07)
				continue ;
			}

			// 途中をカットした為、もう一度計算しなおし。
			// フォーマットがおかしい場合の処理
			if( end < 0 ) {
				final String errMsg = "このテンプレートファイルの {@XXXX} が、フォーマットエラーです。"
								+ CR
								+ chLine.substring( st ) ;
				throw new HybsSystemException( errMsg );
			}

			final String key = buf.substring( st+2,end );

			final String val = getValue( key );
			if( val.indexOf( "  " ) >= 0 ) { spaceInFlag = true; }

			// {@XXXX} を 実際の値と置き換える。
			buf.replace( st,end+1,val );

			// {@ の 存在チェック。
			st = buf.indexOf( "{@",st-1 );		// 3.5.5.9 (2004/06/07)
		}

		// 3.6.0.0 (2004/09/17) pageEndCut が true の場合は、PAGE_END_CUT 文字列のある行を削除します。
		// ここで判定するのは、PAGE_END_CUT 文字そのものが、加工されている可能性があるため。
		String rtn = buf.toString();

		final boolean flag = USE_DATAOVER ? dataOver : rowOver ; // 5.7.1.0 (2013/12/06)

		if( flag && pageEndCut ) {		// 5.7.1.0 (2013/12/06)
			final String temp = rtn.replaceAll( CUT_TAG1 + "[^>]*>" ,"" );
			if( temp.indexOf( PAGE_END_CUT ) >= 0 ) {
				rtn = "" ;
			}
		}
		else {
			// 3.6.0.0 (2004/09/17) スペース置き換えは、<td XXX>YYY</td> の YYYの範囲のみとする。
			if( spaceInFlag ) {
				rtn = spaceReplace( rtn ) ;
			}
		}
		return rtn ;
	}

	/**
	 * 超特殊処理。
	 * EXCEL の ヘッダー/フッター部分は、\{\&#064;XXXX\} と、エスケープ文字が付加される
	 * ので、この文字列を見つけたら、{&#064;XXXX} に、戻して処理するようにする。
	 *
	 * @param	inLine	入力文字列
	 *
	 * @return	出力文字列
	 * @og.rtnNotNull
	 */
	private String changeHeaderFooterData( final String inLine ) {
		int st = inLine.indexOf( "\\{\\@" );
		if( st < 0 ) { return inLine; }

		final StringBuilder buf = new StringBuilder( inLine );

		while( st >= 0 ) {
			buf.deleteCharAt( st );			// 初めの '\'
			buf.deleteCharAt( st+1 );		// １文字削除している為、+1 番目を削除
			final int end = buf.indexOf( "\\}",st+2 );
			// フォーマットがおかしい場合の処理
			if( end < 0 ) {
				final String errMsg = "このテンプレートの HeaderFooter 部分の {@XXXX} が、書式エラーです。"
								+ CR
								+ inLine ;
				throw new HybsSystemException( errMsg );
			}
			buf.deleteCharAt( end );		// 初めの '\'
			st = buf.indexOf( "\\{\\@",end + 1 );
		}
		return buf.toString();
	}

	/**
	 * 入力文字列 を読み取って、出力します。
	 * 各サブクラスで実装してください。
	 *
	 * @param line 入力文字列
	 */
	@Override
	protected void println( final String line ) {
		writer.println( line );
	}

	/**
	 * {&#064;XXXX}文字変換後のスペースを、&amp;nbsp;と置き換えます。
	 *
	 * ただし、式などを使用すると、td タグの属性情報に{&#064;XXXX}文字が含まれ
	 * これに、EXCELのスペースである、&lt;span style="mso-spacerun:
	 * yes"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;
	 * と置き換えると、属性リスト中のタグという入れ子状態が発生する為、
	 * これは、置き換えません。
	 * &lt;td XXX&gt;YYY&lt;/td&gt; の YYYの範囲 を置き換えることになります。
	 *
	 * ここでは、過去の互換性を最大限確保する為に、特殊な方法で、処理します。
	 * 前後のスペースを取り除いた文字列で、かつ、２つ以上の連続したスペースが
	 * 存在する場合のみ、trim して、連続スペースを、&amp;nbsp;と置き換えます。
	 * 文字の間に連続スペースがない場合は、前後のスペースも削除せずに、
	 * 元の文字列をそのまま返します。
	 * 前後のスペースを変換してしまうと、数字型の場合に、EXCELでの計算式がエラーになります。
	 *
	 * @og.rev 3.5.0.0 (2003/09/17) 新規追加
	 * @og.rev 3.5.5.0 (2004/03/12) 連続スペースの処理をEXCELの方式に合わせる
	 * @og.rev 3.6.0.0 (2004/09/17) スペース置き換えは、<td XXX>YYY</td> の YYYの範囲のみとする。
	 * @og.rev 3.6.1.0 (2005/01/05) 置換ロジック修正(ReplaceString クラスを使用)
	 *
	 * @param	target 元の文字列
	 *
	 * @return	置換えた文字列
	 */
	private String spaceReplace( final String target ) {
		final ReplaceString repData = new ReplaceString();

		final Matcher match1 = PTN1.matcher( target ) ;
		while( match1.find() ) {
			final int st1 = match1.start(1);
			final String grp1 = match1.group(1);
			final Matcher match2 = PTN2.matcher( grp1 ) ;
			while( match2.find() ) {
				final int st2 = match2.start(1);
				final String grp2 = match2.group(1);
				final Matcher match3 = PTN3.matcher( grp2 ) ;
				while( match3.find() ) {

					final int st = st1 + st2 + match3.start(1);
					final int ed = st1 + st2 + match3.end(1);

					repData.add( st,ed,makeSpace( ed-st ) );
				}
			}
		}

		final String rtn = repData.replaceAll( target );

		return rtn ;
	}

	/**
	 * 指定の個数のスペース文字を表す、EXCEL の記号を作成します。
	 *
	 * EXCELでは、スペース２個以上を、&lt;span style="mso-spacerun: yes"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;
	 * 形式に置き換えます。これは、EXCELがHTML変換する時のルールです。
	 *
	 * ここでは、スペースの個数-1 の &amp;nbsp; を持つ、上記の文字列を作成します。
	 * 最後の一つは、本物のスペース記号を割り当てます。
	 *
	 * @og.rev 3.6.0.0 (2004/09/17) 新規追加
	 *
	 * @param	cnt スペースの個数
	 *
	 * @return	置換えた文字列
	 * @og.rtnNotNull
	 */
	private String makeSpace( final int cnt ) {
		final StringBuilder buf = new StringBuilder( 40 + cnt * 6 );
		buf.append( SPACE_ST );
		for( int i=1; i<cnt; i++ ) {
			buf.append( SPACE );
		}
		buf.append( SPACE_ED );

		return buf.toString();
	}

	/**
	 * {&#064;QRCODE.XXXX} を含む 文字列の alt 属性を src 属性にセットします。
	 *
	 * QRコードの画像を入れ替えるため、alt属性に設定してある キー情報を元に、
	 * ２次元バーコード画像を作成し、そのファイル名を、src 属性に設定することで、
	 * 動的に画像ファイルのリンクを作成します。
	 * 現在のEXCELでは、バージョンによって、２種類の画像表示方法が存在するようで、
	 * １画像に付き、２箇所の変更が必要です。この２箇所は、変換方法が異なる為、
	 * 全く別の処理を行う必要があります。
	 *
	 * &lt;v:shape ･･･ alt="{&#064;QRCODE.XXXX}" ･･･&gt;
	 *   &lt;v:imagedata src="yyy" ･･･&gt;･･･&lt;/v:shape&gt;形式とマッチし、
	 * xxx 部分と、yyy 部分を前方参照します。
	 *
	 * &lt;img ･･･ src="yyy" ･･･ alt="{&#064;QRCODE.XXXX}" ･･･ &gt; 形式とマッチし、
	 * yyy 部分と、xxx 部分を前方参照します。
	 *
	 * 画像のエンコードは、alt属性に設定した、{&#064;QRCODE.XXXX} 文字列の
	 * XXXX 部分のカラムデータ(通常、{&#064;XXXX} で取得できる値)を使用します。
	 * データが存在しない場合は、src="yyy" 部を削除することで対応します。
	 * なお、後続処理の関係で、alt="{&#064;QRCODE.XXXX}" 文字列は、削除します。
	 *
	 * @og.rev 3.6.1.0 (2005/01/05) 新規追加
	 *
	 * @param	target 元の文字列
	 *
	 * @return	置換えた文字列
	 */
	private String qrcodeReplace( final String target ) {
		final ReplaceString repData = new ReplaceString();

		final Matcher match1 = IMGPTN1.matcher( target ) ;
		while( match1.find() ) {
			final String altV = match1.group(1);

			final int stAlt = match1.start(1) - 9 ;	// {@QRCODE. まで遡る
			final int edAlt = match1.end(1)   + 1 ;	// } を含める
			repData.add( stAlt,edAlt,"" );		// {@QRCODE.XXXX} の部分削除

			final int st = match1.start(2);
			final int ed = match1.end(2);

			final String msg = getValue( altV );	// QRコード変換する文字列の取得
			if( msg != null && msg.length() > 0 ) {
				final String newStr = makeQrImage( altV,msg );	// 画像ファイルのファイル名
				repData.add( st,ed,newStr );
			}
			else {
				repData.add( st-5,ed+1,"" );		// src="yyy" 部分のみ削除
			}
		}

		final Matcher match2 = IMGPTN2.matcher( target ) ;
		while( match2.find() ) {
			final int st = match2.start(1);
			final int ed = match2.end(1);

			final String altV = match2.group(2);
			final int stAlt = match2.start(2) - 9 ;	// {@QRCODE. まで遡る
			final int edAlt = match2.end(2)   + 1 ;	// } を含める
			repData.add( stAlt,edAlt,"" );		// {@QRCODE.XXXX} の部分削除

			final String msg = getValue( altV );	// QRコード変換する文字列の取得
			if( msg != null && msg.length() > 0 ) {
				final String newStr = makeQrImage( altV,msg );	// 画像ファイルのファイル名
				repData.add( st,ed,newStr );
			}
			else {
				repData.add( st-5,ed+1,"" );		// src="yyy" 部分のみ削除
			}
		}

		final String rtn = repData.replaceAll( target ) ;

		return rtn ;
	}

	/**
	 * 指定のカラム名と、QRコード変換する文字列より、画像を作成します。
	 *
	 * 返り値は、作成した画像ファイルのファイル名です。
	 * これは、データが存在しない場合に、src="" を返す必要があるため、
	 * (でないと、画像へのリンクが表示されてしまう。)
	 * src="./帳票ID.files/image00x.png" という画像ファイルのアドレス部分を
	 *  {&#064;QRCODE_カラム名} 形式に変更しておく必要があります。
	 *
	 * @og.rev 3.6.1.0 (2005/01/05) 新規追加
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 * @og.rev 6.4.3.2 (2016/02/19) ConcurrentHashMap のkey,valともに、NOT NULL制限あり。
	 *
	 * @param	key カラム名
	 * @param	msg QRコード変換する文字列
	 *
	 * @return	画像ファイルのファイル名
	 */
	private String makeQrImage( final String key, final String msg ) {
		if( key == null || msg == null || msg.isEmpty() ) { return "" ; }

		// 6.4.3.2 (2016/02/19) ConcurrentHashMap のkey,valともに、NOT NULL制限あり。
		String realClmName = key ;
		final int sp = key.lastIndexOf( '_' );
		if( sp >= 0 ) {
			try {
				final int row = Integer.parseInt( key.substring( sp+1 ) );
				final int realRow = getRealRow( row );
				realClmName = key.substring( 0,sp ) + "_" + realRow ;
			}
			catch( final NumberFormatException ex ) {	// 4.0.0 (2005/01/31)
//				final String errMsg = "警告：QRCODE名のヘッダーに'_'カラム名が使用";
				final String errMsg = "警告：QRCODE名のヘッダーに'_'カラム名が使用 key=" + key + " , ERR=" + ex.getMessage() ;		// 6.9.7.0 (2018/05/14) PMD
				LogWriter.log( errMsg );
			}
		}

		// 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
		if( qrFileMap == null ) { qrFileMap = new ConcurrentHashMap<>(); }
		if( qrFileMap.containsKey( realClmName ) ) {	// Map にすでに存在している。
			return qrFileMap.get( realClmName );
		}

		// 帳票ＩＤ を元に、画像ファイルの保存フォルダを求めます。
		final String filename    = "./" + listId + ".files/" + realClmName + ".png";
		final String fullAddress = htmlDir + filename ;

		final QrcodeImage qrImage = new QrcodeImage();
		qrImage.init( msg,fullAddress );
		qrImage.saveImage();

		qrFileMap.put( realClmName,filename );
		return filename;
	}
}
