    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.hayabusa.report;
    017
    018import org.opengion.hayabusa.common.HybsSystem;
    019import org.opengion.hayabusa.common.HybsSystemException;
    020import org.opengion.fukurou.system.LogWriter;
    021import org.opengion.fukurou.util.QrcodeImage;
    022import org.opengion.fukurou.util.ReplaceString;
    023import static org.opengion.fukurou.system.HybsConst.CR ;                                // 6.1.0.0 (2014/12/26)
    024import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
    025
    026import java.io.IOException;
    027import java.util.concurrent.ConcurrentMap;                                                      // 6.4.3.3 (2016/03/04)
    028import java.util.concurrent.ConcurrentHashMap;                                          // 6.4.3.1 (2016/02/12) refactoring
    029import java.util.regex.Pattern;
    030import java.util.regex.Matcher ;
    031
    032/**
    033 * DBTableReport インターフェース を実装したHTMLをパースするクラスです。
    034 * AbstractDBTableReport を継承していますので，writeReport() のみオーバーライドして，
    035 * 固定長文字ファイルの出力機能を実現しています。
    036 *
    037 * @og.group 帳票システム
    038 *
    039 * @version  4.0
    040 * @author   Kazuhiko Hasegawa
    041 * @since    JDK5.0,
    042 */
    043public class DBTableReport_HTML extends AbstractDBTableReport {
    044        private static final String TR_IN        = "<tr" ;
    045        private static final String TR_OUT       = "</tr>" ;
    046        private static final String TD_OUT       = "</td>" ;                    // 3.5.5.9 (2004/06/07)
    047        private static final String PAGE_BREAK   = "page-break" ;
    048        private static final String PAGE_END_CUT = "PAGE_END_CUT" ;             // 3.6.0.0 (2004/09/17)
    049        private static final String END_TAG      = "</table></body></html>";
    050        private static final String CUT_TAG1     = "<span";
    051        private static final String CUT_TAG2     = "</span>";
    052        private static final String SPACE_ST     = "<span style=\"mso-spacerun: yes\">";        // 3.6.0.0 (2004/09/17)
    053        private static final String SPACE        = "&nbsp;";                                                            // 3.6.0.0 (2004/09/17)
    054        private static final String SPACE_ED     = " </span>";                                                  // 3.6.0.0 (2004/09/17)
    055        private static final String FRAMESET     = "Excel Workbook Frameset" ;
    056
    057        // <td xxx="yyy">zzzz</td> 形式とマッチし、>zzzz< 部分を前方参照します。
    058        private static final Pattern PTN1 = Pattern.compile("<td[^>]*(>.*?<)/td>");
    059        // >aaaa<span bb="cc">dddd</span>eeee< 形式に２文字以上のスペースを含むデータと
    060        // マッチし、aaaa,dddd,eeee を前方参照します。
    061        private static final Pattern PTN2 = Pattern.compile("[^>]*>([^<]*?  ++[^<]*?)<");
    062        // aa   bb    cc    形式とマッチし、各連続スペース部分を前方参照します。
    063        private static final Pattern PTN3 = Pattern.compile("(  +)");
    064
    065        private boolean  fileEnd                ;               // ファイルの読み取り制御
    066
    067        // 3.6.1.0 (2005/01/05) QRコード(２次元バーコード)用の出力ファイル管理
    068        /** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
    069        private ConcurrentMap<String,String> qrFileMap  ;
    070        // <v:shape ･･･ alt="{@QRCODE.XXXX}" ･･･>
    071        //   <v:imagedata src="yyy" ･･･>･･･</v:shape>形式とマッチし、
    072        // xxx 部分と、yyy 部分を前方参照します。
    073        private static final Pattern IMGPTN1 = Pattern.compile("<v:shape [^>]*alt=\"\\{@QRCODE.([^\\}]*)\\}\"[^>]*>[^<]*<v:imagedata [^>]*src=\"([^\"]*)\"[^>]*>");
    074        // <img ･･･ src="yyy" ･･･ alt="{@QRCODE.XXXX}" ･･･ > 形式とマッチし、
    075        // yyy 部分と、xxx 部分を前方参照します。
    076        private static final Pattern IMGPTN2 = Pattern.compile("<img [^>]*src=\"([^\"]*)\"[^>]*alt=\"\\{@QRCODE.([^\\}]*)\\}\"[^>]*>");
    077
    078        // 4.0.0 (2007/06/08) pageEndCut = true  時の LINE_COPY 機能の実装
    079        private static final String LINE_COPY = "LINE_COPY" ;           // 4.0.0 (2007/06/08)
    080        private String lineCopy ;
    081
    082        // 5.7.1.0 (2013/12/06) trueの場合 PAGE_END_CUTの判定にdataOver フラグを使用。falseの場合は、rowOver を使用。
    083        private final boolean USE_DATAOVER = HybsSystem.sysBool( "COMPATIBLE_PAGE_END_CUT_RETRIEVAL" );
    084
    085        /**
    086         * デフォルトコンストラクター
    087         *
    088         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
    089         */
    090        public DBTableReport_HTML() { super(); }                // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
    091
    092        /**
    093         * 入力文字列 を読み取って、出力します。
    094         * tr タグを目印に、１行(trタグ間)ずつ取り出します。
    095         * 読み取りを終了する場合は、null を返します。
    096         * 各サブクラスで実装してください。
    097         *
    098         * @og.rev 3.0.0.1 (2003/02/14) 一度もValueセットしていないのに次ページ要求があった場合は、フォーマットがおかしい
    099         * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、親クラスに移動します。
    100         *
    101         * @return      出力文字列
    102         */
    103        @Override
    104        protected String readLine() {
    105                if( fileEnd ) { return null; }
    106
    107                // pageEndCut 時に、データがオーバーしていない間のみ、lineCopy があれば返す。
    108                if( pageEndCut && !rowOver && lineCopy != null ) {
    109                        lineCopyCnt ++ ;        // 雛形は、_0 のみが毎回返される為の、加算
    110                        return lineCopy ;
    111                }
    112
    113                final StringBuilder buf ;
    114                try {
    115                        String line = reader.readLine();
    116                        if( line == null ) {
    117                                if( rowOver ) {
    118                                        return null;
    119                                }
    120                                else {
    121                                        initReader();
    122                                        initWriter();
    123                                        line = reader.readLine();
    124                                        if( line == null ) { return null; }
    125                                }
    126                        }
    127                        if( line.indexOf( FRAMESET )  >= 0 ) {
    128                                final String errMsg = "HTML ファイルエラー :" + line + CR
    129                                                                + "Excelファイル形式がフレームになっています。(複数シートには未対応)" ;
    130                                throw new HybsSystemException( errMsg );
    131                        }
    132                        if( line.indexOf( TR_IN )  >= 0 ) {
    133                                buf = new StringBuilder( BUFFER_MIDDLE );
    134                                buf.append( line );
    135                                int trLevel = 1;                        // 行を表す <tr> のレベル
    136                                while( trLevel != 0 ) {
    137                                        line = reader.readLine();
    138                                        // 4.0.0 (2005/08/31) null 参照はずし対応
    139                                        // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
    140                                        if( line == null ) {
    141                                                final String errMsg = "HTML ファイルエラー :" + buf.toString() + CR
    142                                                                        + "行(TR)の整合性が取れる前に、ファイルが終了しました。" ;
    143                                                throw new HybsSystemException( errMsg );
    144                                        }
    145                                        if( line.indexOf( TR_IN  ) >= 0 ) { trLevel++ ; }
    146                                        if( line.indexOf( TR_OUT ) >= 0 ) { trLevel-- ; }
    147                                        buf.append( CR ).append( line );
    148
    149                                }
    150                        }
    151                        else {
    152                                return line;
    153                        }
    154                } catch( final IOException ex ) {
    155                        final String errMsg = "HTML ファイル 読取時にエラーが発生しました。" + reader;
    156                        throw new HybsSystemException( errMsg,ex );             // 3.5.5.4 (2004/04/15) 引数の並び順変更
    157                }
    158
    159                String rtnLine = buf.toString() ;
    160
    161                // lineCopy 情報の取得。
    162                if( pageEndCut && !rowOver ) {
    163                        // LINE_COPY は削除しますので、表示上見えるようにしておいてください。
    164                        final int adrs = rtnLine.indexOf( LINE_COPY );
    165                        if( adrs >= 0 ) {
    166                                lineCopy = rtnLine.substring( 0,adrs )
    167                                                        + rtnLine.substring( adrs + LINE_COPY.length() ) ;
    168                                rtnLine = lineCopy ;
    169                        }
    170                }
    171
    172                return rtnLine ;
    173        }
    174
    175        /**
    176         * 入力文字列 を加工して、出力します。
    177         * {&#064;XXXX} をテーブルモデルより読み取り、値をセットします。
    178         * 各サブクラスで実装してください。
    179         *
    180         * @og.rev 3.0.0.1 (2003/02/14) 一度もValueセットしていないのに次ページ要求があった場合は、フォーマットがおかしい
    181         * @og.rev 3.0.0.2 (2003/02/20) {&#064;XXXX}文字が、EXCELに表示しきれない場合に挿入されるタグの削除処理の変更。
    182         * @og.rev 3.5.0.0 (2003/09/17) {&#064;XXXX}文字のスペースを、&amp;nbsp;と置き換えます。
    183         * @og.rev 3.5.0.0 (2003/09/17) {&#064;XXXX}文字がアンバランス時にHybsSystemExceptionを発行する。
    184         * @og.rev 3.5.5.9 (2004/06/07) {&#064;XXXX}の連続処理のアドレス計算方法が、間違っていましたので修正します。
    185         * @og.rev 3.6.0.0 (2004/09/17) pageEndCut が true の場合は、PAGE_END_CUT 文字列のある行を削除します。
    186         * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、親クラスに移動します。
    187         * @og.rev 3.6.1.0 (2005/01/05) QRコード(２次元バーコード)の機能追加
    188         * @og.rev 3.8.1.2 (2005/12/19) PAGE_END_CUTの判定にdataOver フラグを使用。
    189         * @og.rev 5.7.1.0 (2013/12/06) USE_DATAOVER が trueの場合 PAGE_END_CUTの判定にdataOver フラグを使用。falseの場合は、rowOver を使用
    190         *
    191         * @param       inLine  入力文字列
    192         *
    193         * @return      出力文字列
    194         * @og.rtnNotNull
    195         */
    196        @Override
    197        protected String changeData( final String inLine ) {
    198                // rowOver で、かつ ページブレークかページエンドカットの場合、処理終了。
    199                if( rowOver && inLine.indexOf( PAGE_BREAK ) >= 0 ) {
    200                        fileEnd = true;
    201                        return END_TAG;
    202                }
    203
    204                String chLine = changeHeaderFooterData( inLine ) ;
    205
    206                // 3.6.1.0 (2005/01/05) QRコード(２次元バーコード)の機能追加
    207                if( chLine.indexOf( "{@QRCODE." ) >= 0 ) {
    208                        chLine = qrcodeReplace( chLine );
    209                }
    210
    211                int st = chLine.indexOf( "{@" );
    212                // 3.8.1.2 (2005/12/19) {@XXXX}の存在しない行も PAGE_END_CUTの判定を行う。
    213
    214                final StringBuilder buf = new StringBuilder( chLine );
    215
    216                boolean spaceInFlag = false;    // {@XXXX} 変数のデータにスペースを含むかどうかチェック
    217                while( st >= 0 ) {
    218                        int end = buf.indexOf( "}",st+2 );
    219
    220                        // EXCELに表示しきれない文字は、CUT_TAG1,CUT_TAG2 が挿入されてしまう為、
    221                        // 削除する必要がある。
    222                        final int cutSt1 = buf.indexOf( CUT_TAG1,st+2 );
    223                        if( cutSt1 >= 0 && cutSt1 < end ) {
    224                                final int cutEnd1 = buf.indexOf( ">",cutSt1 );
    225
    226                                final int cutSt2 = buf.indexOf( CUT_TAG2,end );
    227                                if( cutSt2 >= 0 ) {
    228                                        buf.delete( cutSt2, cutSt2 + CUT_TAG2.length() );
    229                                }
    230                                buf.delete( cutSt1, cutEnd1+1 );
    231                                // 途中をカットした為、もう一度計算しなおし。
    232                                end = buf.indexOf( "}",st+2 );          // 3.5.5.9 (2004/06/07)
    233                        }
    234
    235                        // 3.5.5.9 (2004/06/07)
    236                        // 関数等を使用すると、{@XXXX} 文字列を直接加工したデータが出力される。
    237                        // この加工されたデータは、HTML 表示に使用されるだけのため、削除します。
    238                        // 削除方法は、{@XXX</td> を想定している為、 {@ から </td> の間です。
    239                        final int td_out = buf.indexOf( TD_OUT,st+2 );
    240                        if( td_out >= 0 && td_out < end ) {
    241                                buf.delete( st, td_out );
    242                                // {@XXXX} パラメータが消えたので、次の計算を行います。
    243                                st = buf.indexOf( "{@",st+4 );          // 3.5.5.9 (2004/06/07)
    244                                continue ;
    245                        }
    246
    247                        // 途中をカットした為、もう一度計算しなおし。
    248                        // フォーマットがおかしい場合の処理
    249                        if( end < 0 ) {
    250                                final String errMsg = "このテンプレートファイルの {@XXXX} が、フォーマットエラーです。"
    251                                                                + CR
    252                                                                + chLine.substring( st ) ;
    253                                throw new HybsSystemException( errMsg );
    254                        }
    255
    256                        final String key = buf.substring( st+2,end );
    257
    258                        final String val = getValue( key );
    259                        if( val.indexOf( "  " ) >= 0 ) { spaceInFlag = true; }
    260
    261                        // {@XXXX} を 実際の値と置き換える。
    262                        buf.replace( st,end+1,val );
    263
    264                        // {@ の 存在チェック。
    265                        st = buf.indexOf( "{@",st-1 );          // 3.5.5.9 (2004/06/07)
    266                }
    267
    268                // 3.6.0.0 (2004/09/17) pageEndCut が true の場合は、PAGE_END_CUT 文字列のある行を削除します。
    269                // ここで判定するのは、PAGE_END_CUT 文字そのものが、加工されている可能性があるため。
    270                String rtn = buf.toString();
    271
    272                final boolean flag = USE_DATAOVER ? dataOver : rowOver ; // 5.7.1.0 (2013/12/06)
    273
    274                if( flag && pageEndCut ) {              // 5.7.1.0 (2013/12/06)
    275                        final String temp = rtn.replaceAll( CUT_TAG1 + "[^>]*>" ,"" );
    276                        if( temp.indexOf( PAGE_END_CUT ) >= 0 ) {
    277                                rtn = "" ;
    278                        }
    279                }
    280                else {
    281                        // 3.6.0.0 (2004/09/17) スペース置き換えは、<td XXX>YYY</td> の YYYの範囲のみとする。
    282                        if( spaceInFlag ) {
    283                                rtn = spaceReplace( rtn ) ;
    284                        }
    285                }
    286                return rtn ;
    287        }
    288
    289        /**
    290         * 超特殊処理。
    291         * EXCEL の ヘッダー/フッター部分は、\{\&#064;XXXX\} と、エスケープ文字が付加される
    292         * ので、この文字列を見つけたら、{&#064;XXXX} に、戻して処理するようにする。
    293         *
    294         * @param       inLine  入力文字列
    295         *
    296         * @return      出力文字列
    297         * @og.rtnNotNull
    298         */
    299        private String changeHeaderFooterData( final String inLine ) {
    300                int st = inLine.indexOf( "\\{\\@" );
    301                if( st < 0 ) { return inLine; }
    302
    303                final StringBuilder buf = new StringBuilder( inLine );
    304
    305                while( st >= 0 ) {
    306                        buf.deleteCharAt( st );                 // 初めの '\'
    307                        buf.deleteCharAt( st+1 );               // １文字削除している為、+1 番目を削除
    308                        final int end = buf.indexOf( "\\}",st+2 );
    309                        // フォーマットがおかしい場合の処理
    310                        if( end < 0 ) {
    311                                final String errMsg = "このテンプレートの HeaderFooter 部分の {@XXXX} が、書式エラーです。"
    312                                                                + CR
    313                                                                + inLine ;
    314                                throw new HybsSystemException( errMsg );
    315                        }
    316                        buf.deleteCharAt( end );                // 初めの '\'
    317                        st = buf.indexOf( "\\{\\@",end + 1 );
    318                }
    319                return buf.toString();
    320        }
    321
    322        /**
    323         * 入力文字列 を読み取って、出力します。
    324         * 各サブクラスで実装してください。
    325         *
    326         * @param line 入力文字列
    327         */
    328        @Override
    329        protected void println( final String line ) {
    330                writer.println( line );
    331        }
    332
    333        /**
    334         * {&#064;XXXX}文字変換後のスペースを、&amp;nbsp;と置き換えます。
    335         *
    336         * ただし、式などを使用すると、td タグの属性情報に{&#064;XXXX}文字が含まれ
    337         * これに、EXCELのスペースである、&lt;span style="mso-spacerun:
    338         * yes"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;
    339         * と置き換えると、属性リスト中のタグという入れ子状態が発生する為、
    340         * これは、置き換えません。
    341         * &lt;td XXX&gt;YYY&lt;/td&gt; の YYYの範囲 を置き換えることになります。
    342         *
    343         * ここでは、過去の互換性を最大限確保する為に、特殊な方法で、処理します。
    344         * 前後のスペースを取り除いた文字列で、かつ、２つ以上の連続したスペースが
    345         * 存在する場合のみ、trim して、連続スペースを、&amp;nbsp;と置き換えます。
    346         * 文字の間に連続スペースがない場合は、前後のスペースも削除せずに、
    347         * 元の文字列をそのまま返します。
    348         * 前後のスペースを変換してしまうと、数字型の場合に、EXCELでの計算式がエラーになります。
    349         *
    350         * @og.rev 3.5.0.0 (2003/09/17) 新規追加
    351         * @og.rev 3.5.5.0 (2004/03/12) 連続スペースの処理をEXCELの方式に合わせる
    352         * @og.rev 3.6.0.0 (2004/09/17) スペース置き換えは、<td XXX>YYY</td> の YYYの範囲のみとする。
    353         * @og.rev 3.6.1.0 (2005/01/05) 置換ロジック修正(ReplaceString クラスを使用)
    354         *
    355         * @param       target 元の文字列
    356         *
    357         * @return      置換えた文字列
    358         */
    359        private String spaceReplace( final String target ) {
    360                final ReplaceString repData = new ReplaceString();
    361
    362                final Matcher match1 = PTN1.matcher( target ) ;
    363                while( match1.find() ) {
    364                        final int st1 = match1.start(1);
    365                        final String grp1 = match1.group(1);
    366                        final Matcher match2 = PTN2.matcher( grp1 ) ;
    367                        while( match2.find() ) {
    368                                final int st2 = match2.start(1);
    369                                final String grp2 = match2.group(1);
    370                                final Matcher match3 = PTN3.matcher( grp2 ) ;
    371                                while( match3.find() ) {
    372
    373                                        final int st = st1 + st2 + match3.start(1);
    374                                        final int ed = st1 + st2 + match3.end(1);
    375
    376                                        repData.add( st,ed,makeSpace( ed-st ) );
    377                                }
    378                        }
    379                }
    380
    381                final String rtn = repData.replaceAll( target );
    382
    383                return rtn ;
    384        }
    385
    386        /**
    387         * 指定の個数のスペース文字を表す、EXCEL の記号を作成します。
    388         *
    389         * EXCELでは、スペース２個以上を、&lt;span style="mso-spacerun: yes"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;
    390         * 形式に置き換えます。これは、EXCELがHTML変換する時のルールです。
    391         *
    392         * ここでは、スペースの個数-1 の &amp;nbsp; を持つ、上記の文字列を作成します。
    393         * 最後の一つは、本物のスペース記号を割り当てます。
    394         *
    395         * @og.rev 3.6.0.0 (2004/09/17) 新規追加
    396         *
    397         * @param       cnt スペースの個数
    398         *
    399         * @return      置換えた文字列
    400         * @og.rtnNotNull
    401         */
    402        private String makeSpace( final int cnt ) {
    403                final StringBuilder buf = new StringBuilder( 40 + cnt * 6 );
    404                buf.append( SPACE_ST );
    405                for( int i=1; i<cnt; i++ ) {
    406                        buf.append( SPACE );
    407                }
    408                buf.append( SPACE_ED );
    409
    410                return buf.toString();
    411        }
    412
    413        /**
    414         * {&#064;QRCODE.XXXX} を含む 文字列の alt 属性を src 属性にセットします。
    415         *
    416         * QRコードの画像を入れ替えるため、alt属性に設定してある キー情報を元に、
    417         * ２次元バーコード画像を作成し、そのファイル名を、src 属性に設定することで、
    418         * 動的に画像ファイルのリンクを作成します。
    419         * 現在のEXCELでは、バージョンによって、２種類の画像表示方法が存在するようで、
    420         * １画像に付き、２箇所の変更が必要です。この２箇所は、変換方法が異なる為、
    421         * 全く別の処理を行う必要があります。
    422         *
    423         * &lt;v:shape ･･･ alt="{&#064;QRCODE.XXXX}" ･･･&gt;
    424         *   &lt;v:imagedata src="yyy" ･･･&gt;･･･&lt;/v:shape&gt;形式とマッチし、
    425         * xxx 部分と、yyy 部分を前方参照します。
    426         *
    427         * &lt;img ･･･ src="yyy" ･･･ alt="{&#064;QRCODE.XXXX}" ･･･ &gt; 形式とマッチし、
    428         * yyy 部分と、xxx 部分を前方参照します。
    429         *
    430         * 画像のエンコードは、alt属性に設定した、{&#064;QRCODE.XXXX} 文字列の
    431         * XXXX 部分のカラムデータ(通常、{&#064;XXXX} で取得できる値)を使用します。
    432         * データが存在しない場合は、src="yyy" 部を削除することで対応します。
    433         * なお、後続処理の関係で、alt="{&#064;QRCODE.XXXX}" 文字列は、削除します。
    434         *
    435         * @og.rev 3.6.1.0 (2005/01/05) 新規追加
    436         *
    437         * @param       target 元の文字列
    438         *
    439         * @return      置換えた文字列
    440         */
    441        private String qrcodeReplace( final String target ) {
    442                final ReplaceString repData = new ReplaceString();
    443
    444                final Matcher match1 = IMGPTN1.matcher( target ) ;
    445                while( match1.find() ) {
    446                        final String altV = match1.group(1);
    447
    448                        final int stAlt = match1.start(1) - 9 ; // {@QRCODE. まで遡る
    449                        final int edAlt = match1.end(1)   + 1 ; // } を含める
    450                        repData.add( stAlt,edAlt,"" );          // {@QRCODE.XXXX} の部分削除
    451
    452                        final int st = match1.start(2);
    453                        final int ed = match1.end(2);
    454
    455                        final String msg = getValue( altV );    // QRコード変換する文字列の取得
    456                        if( msg != null && msg.length() > 0 ) {
    457                                final String newStr = makeQrImage( altV,msg );  // 画像ファイルのファイル名
    458                                repData.add( st,ed,newStr );
    459                        }
    460                        else {
    461                                repData.add( st-5,ed+1,"" );            // src="yyy" 部分のみ削除
    462                        }
    463                }
    464
    465                final Matcher match2 = IMGPTN2.matcher( target ) ;
    466                while( match2.find() ) {
    467                        final int st = match2.start(1);
    468                        final int ed = match2.end(1);
    469
    470                        final String altV = match2.group(2);
    471                        final int stAlt = match2.start(2) - 9 ; // {@QRCODE. まで遡る
    472                        final int edAlt = match2.end(2)   + 1 ; // } を含める
    473                        repData.add( stAlt,edAlt,"" );          // {@QRCODE.XXXX} の部分削除
    474
    475                        final String msg = getValue( altV );    // QRコード変換する文字列の取得
    476                        if( msg != null && msg.length() > 0 ) {
    477                                final String newStr = makeQrImage( altV,msg );  // 画像ファイルのファイル名
    478                                repData.add( st,ed,newStr );
    479                        }
    480                        else {
    481                                repData.add( st-5,ed+1,"" );            // src="yyy" 部分のみ削除
    482                        }
    483                }
    484
    485                final String rtn = repData.replaceAll( target ) ;
    486
    487                return rtn ;
    488        }
    489
    490        /**
    491         * 指定のカラム名と、QRコード変換する文字列より、画像を作成します。
    492         *
    493         * 返り値は、作成した画像ファイルのファイル名です。
    494         * これは、データが存在しない場合に、src="" を返す必要があるため、
    495         * (でないと、画像へのリンクが表示されてしまう。)
    496         * src="./帳票ID.files/image00x.png" という画像ファイルのアドレス部分を
    497         *  {&#064;QRCODE_カラム名} 形式に変更しておく必要があります。
    498         *
    499         * @og.rev 3.6.1.0 (2005/01/05) 新規追加
    500         * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
    501         * @og.rev 6.4.3.2 (2016/02/19) ConcurrentHashMap のkey,valともに、NOT NULL制限あり。
    502         *
    503         * @param       key カラム名
    504         * @param       msg QRコード変換する文字列
    505         *
    506         * @return      画像ファイルのファイル名
    507         */
    508        private String makeQrImage( final String key, final String msg ) {
    509                if( key == null || msg == null || msg.isEmpty() ) { return "" ; }
    510
    511                // 6.4.3.2 (2016/02/19) ConcurrentHashMap のkey,valともに、NOT NULL制限あり。
    512                String realClmName = key ;
    513                final int sp = key.lastIndexOf( '_' );
    514                if( sp >= 0 ) {
    515                        try {
    516                                final int row = Integer.parseInt( key.substring( sp+1 ) );
    517                                final int realRow = getRealRow( row );
    518                                realClmName = key.substring( 0,sp ) + "_" + realRow ;
    519                        }
    520                        catch( final NumberFormatException ex ) {       // 4.0.0 (2005/01/31)
    521//                              final String errMsg = "警告：QRCODE名のヘッダーに'_'カラム名が使用";
    522                                final String errMsg = "警告：QRCODE名のヘッダーに'_'カラム名が使用 key=" + key + " , ERR=" + ex.getMessage() ;           // 6.9.7.0 (2018/05/14) PMD
    523                                LogWriter.log( errMsg );
    524                        }
    525                }
    526
    527                // 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
    528                if( qrFileMap == null ) { qrFileMap = new ConcurrentHashMap<>(); }
    529                if( qrFileMap.containsKey( realClmName ) ) {    // Map にすでに存在している。
    530                        return qrFileMap.get( realClmName );
    531                }
    532
    533                // 帳票ＩＤ を元に、画像ファイルの保存フォルダを求めます。
    534                final String filename    = "./" + listId + ".files/" + realClmName + ".png";
    535                final String fullAddress = htmlDir + filename ;
    536
    537                final QrcodeImage qrImage = new QrcodeImage();
    538                qrImage.init( msg,fullAddress );
    539                qrImage.saveImage();
    540
    541                qrFileMap.put( realClmName,filename );
    542                return filename;
    543        }
    544}