    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.fukurou.xml;
    017
    018import java.io.File;
    019import java.io.IOException;
    020import java.io.BufferedReader;
    021import java.util.concurrent.ConcurrentMap;                                                      // 6.4.3.3 (2016/03/04)
    022import java.util.concurrent.ConcurrentHashMap;                                          // 6.4.3.3 (2016/03/04)
    023
    024import org.opengion.fukurou.util.FileUtil;
    025import org.opengion.fukurou.system.Closer;
    026import org.opengion.fukurou.system.LogWriter;
    027import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    028import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
    029
    030/**
    031 * このクラスは、jspファイルのXSLT変換に特化した、Readerオブジェクトを作成するクラスです。
    032 * jspファイル に記述される、jsp:directive.include を見つけて、そのファイル属性に
    033 * 記述されているファイルを、インクルードします。
    034 * Tomcat の特性上、インクルード時のファイルは、＆等のエスケープを処理しておく
    035 * 必要があります。
    036 * エスケープの前処理は、jsp:root タグのあるなしで判定します。
    037 * 現時点では、 &amp;amp; , &lt; , &lt;= , &gt; , &gt;= を前処理します。
    038 *
    039 * JSP では、og:head タグで、&lt;html&gt; を出力したり、htmlend.jsp インクルードで
    040 * &lt;/body&gt;&lt;/html&gt; を出力していますが、フレームや、フォワードなど、整合性が
    041 * 取れないケースがありますので、XML処理用として、&lt;html&gt; を出力していません。
    042 * 変換結果を、正式な HTML ファイルとして再利用される場合は、ご注意ください。
    043 *
    044 * なお、このクラスは、マルチスレッド対応されていません。
    045 *
    046 * @og.rev 4.0.0.2 (2007/12/10) 新規追加
    047 *
    048 * @version  4.0
    049 * @author   Kazuhiko Hasegawa
    050 * @since    JDK5.0,
    051 */
    052public class JspIncludeReader {
    053        // 6.4.3.3 (2016/03/04) includeファイルは、共通ファイルなので、データ量は多いが数は少ない。
    054        private static final ConcurrentMap<String,String> INCLUDE_FILES = new ConcurrentHashMap<>();                            // 6.4.3.3 (2016/03/04)
    055
    056        // 5.6.7.1 (2013/08/09) デバッグ用にincludeしたファイルを保存しておきます。
    057        private final StringBuilder incFiles = new StringBuilder( BUFFER_MIDDLE );
    058
    059        // 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
    060        private String realPath ;
    061
    062        // タグの属性の値のみを抜き出しています。特に、<>& を含む場合。
    063        // 5.2.1.0 (2010/10/01) 仮廃止
    064        //      private static final Pattern ptn = Pattern.compile( "=[ \t]*\"([^\"]*[<>&].[^\"]*)\"" );
    065
    066        /**
    067         * JSP のインクルードを考慮した、JSPファイルを、String で返します。
    068         * このメソッドは、内部で再帰定義されています。つまり、jsp:directive.include
    069         * 文字列が見つかった場合は、その代わりに、ファイル名を取出して、もう一度
    070         * このメソッドを呼び出します。インクルードファイルとの関連をチェックする為に
    071         * ダミーのspanタグを入れておきます。
    072         * &lt;span type="jsp:directive" include="ファイル名"&gt;&lt;!-- --&gt;&lt;/span&gt;
    073         * ただし、ソースチェック時に、
    074         * Ver4 以降で、インクルードファイルに、XML宣言と、jsp:root を付与するケースがあります。
    075         * 擬似的に取り込むときには、XML宣言は削除します。
    076         *
    077         * @og.rev 5.2.1.0 (2010/10/01) directive.include で、XMLタグとroot タグは取り込まない。
    078         * @og.rev 5.2.1.0 (2010/10/01) エスケープ処理の引数を廃止します。
    079         * @og.rev 5.6.5.2 (2013/06/21) 小細工内容の変更。replaceAll にするのと、スペースまたはタブを使用します。
    080         * @og.rev 5.6.7.1 (2013/08/09) コメントの処理のバグ修正。includeファイル名保存。
    081         * @og.rev 5.6.7.1 (2013/08/09) includeファイルが存在しない場合は、gf共有から取得する。
    082         * @og.rev 5.6.7.2 (2013/08/16) includeファイルを取り込む場合、代わりのspanタグを出力しておきます。
    083         * @og.rev 5.6.7.4 (2013/08/30) includeファイルの先頭のpageEncoding指定のチェック用 span タグの出力
    084         * @og.rev 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
    085         * @og.rev 6.3.9.1 (2015/11/27) htmlend.jsp をインクルード処理しない箇所で、判定方法を、htmlend を含むかどうかに変更。
    086         * @og.rev 6.4.3.2 (2016/02/19) /jsp/*** で始まるファイルのみ、キャッシュします。
    087         * @og.rev 8.1.1.2 (2022/02/25) 1行に複数コメントの不具合対応 (例: --&gt; XXX &lt;!-- YYY --&gt;)
    088         *
    089         * @param       file    JSPファイル
    090         * @param       encode  ファイルのエンコード
    091         *
    092         * @return      インクルードを考慮した、JSPファイル
    093         * @og.rtnNotNull
    094         */
    095        public String getString( final File file,final String encode ) {
    096                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE ) ;
    097                final BufferedReader reader = FileUtil.getBufferedReader( file,encode );
    098
    099                // ファイルが、jsp 直下かどうかを判断します。
    100                final String parentFile = file.getParent() ;
    101                final boolean isUnder = parentFile.endsWith( "\\jsp" );
    102
    103                int  cmntIn    = -1;
    104                int  cmntOut   = -1;
    105                boolean isCmnt = false;                                                                                                 // true:コメント内 false:通常
    106                boolean isEscape = true;        // エスケープするかどうか(true:する/false:しない)
    107                try {
    108                        String line ;
    109                        cmntA: while((line = reader.readLine()) != null) {
    110                                // 5.2.1.0 (2010/10/01) directive.include で、XMLタグは取り込まない。
    111                                if( line.indexOf( "<?xml" ) >= 0 && line.indexOf( "?>" ) >= 0 ) { continue; }
    112                                // jsp:root があれば、エスケープ処理を行わない
    113                                if( line.indexOf( "<jsp:root" ) >= 0 ) { isEscape = false; }
    114
    115                                // コメントの削除
    116                                while(true) {                                                                                                   // 8.1.1.2 (2022/02/21) Add
    117                                        cmntIn  = line.indexOf( "<!--" );
    118                                        cmntOut = line.indexOf( "-->" );
    119                                        // コメント開始＆終了有り 8.1.1.2 (2022/02/21) Modify
    120                                        if( cmntIn >= 0 && cmntOut >= 0 ) {
    121                                                // コメント終了(複数行)
    122                                                if( isCmnt) {
    123                                                        line = line.substring( cmntOut+3 );                                     // 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
    124                                                // コメント開始／終了(1行)
    125                                                } else {
    126                                                        line = line.substring( 0,cmntIn ) + line.substring( cmntOut+3 );        // 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
    127                                                }
    128                                                isCmnt = false;                                                                                 // コメント部分削除済
    129                                        }
    130                                        // コメント開始有り＆終了無し(複数行のコメント開始)
    131                                        else if( cmntIn >= 0 && cmntOut < 0 ) {
    132                                                line = line.substring( 0,cmntIn );
    133                                                isCmnt = true;
    134                                                break;                                                                                                  // 8.1.1.2 (2022/02/21) Add
    135                                        }
    136                                        // コメント開始無し＆終了有り(複数行のコメント終了)
    137                                        else if( cmntIn < 0  && cmntOut >= 0 ) {
    138                                                line = line.substring( cmntOut+3 );                                             // 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
    139                                                isCmnt = false;
    140                                        }
    141                                        // コメント開始＆終了無し
    142                                        else {
    143                                                if( isCmnt ) { continue cmntA; }                                                // コメント内
    144                                                else { break; }                                                                                 // 8.1.1.2 (2022/02/21) Add
    145                                        }
    146                                }                                                                                                                               // 8.1.1.2 (2022/02/21) Add
    147
    148                                // 特殊処理：og:head で html タグを出力している。
    149        //                      if( line.indexOf( "<og:head" ) >= 0 ) {
    150        //                              buf.append( "<html>" );
    151        //                      }
    152
    153                                if( isEscape ) {
    154                                        // 5.6.5.2 (2013/06/21) 小細工内容の変更。replaceAll にするのと、スペースまたはタブを使用します。
    155                                        // & , < , <= , > , >= を前処理します。
    156                                        line = line.replaceAll( "&"  ,"&amp;" );                                // ちょっと小細工
    157                                        line = line.replaceAll( "[ \\t]<[ \\t]"," &lt; " );             // ちょっと小細工
    158                                        line = line.replaceAll( "[ \\t]>[ \\t]"," &gt; " );             // ちょっと小細工
    159                                        line = line.replaceAll( "[ \\t]<="," &lt;=" );                  // ちょっと小細工
    160                                        line = line.replaceAll( "[ \\t]>="," &gt;=" );                  // ちょっと小細工
    161        // 5.2.1.0 (2010/10/01) 仮廃止
    162        //                              Matcher mtch = ptn.matcher( line );
    163        //                              int adrs = 0;
    164        //                              StringBuilder buf2 = new StringBuilder();
    165        //                              while( mtch.find(adrs) ) {
    166        //                                      String grp = mtch.group(1);
    167        //                                      String htm = StringUtil.htmlFilter( grp );
    168        //                                      int in = mtch.start(1);
    169        //                                      buf2.append( line.substring( adrs,in ) ).append( htm );
    170        //                                      adrs = mtch.end(1);
    171        //                              }
    172        //                              buf2.append( line.substring( adrs ) );
    173        //                              line = buf2.toString();
    174                                }
    175
    176                                final int st = line.indexOf( "<jsp:directive.include" );
    177                                if( st < 0 ) { buf.append( line ); }    // include が無ければ、そのまま追加
    178                                else {
    179                                        buf.append( line.substring( 0,st ) );
    180                                        final int fin = line.indexOf( '\"',st );                        // ファイルの最初
    181                                        final int fout= line.indexOf( '\"',fin+1 );                     // ファイルの最後
    182                                        final String fname = line.substring( fin+1,fout );      // ファイル名
    183
    184                                        // 5.6.7.2 (2013/08/16) includeファイルを取り込む場合、代わりのspanタグを出力しておきます。
    185                                        buf.append( "<span type=\"jsp:directive\" include=\"" )
    186                                                .append( fname ).append( "\" ><!-- --></span>" ) ;
    187
    188                                        // htmlend.jsp の インクルードは行わない。
    189        //                              if( fname.endsWith( "htmlend.jsp" ) ) {
    190                                        if( fname.contains( "htmlend" ) ) {                                     // 6.3.9.1 (2015/11/27)
    191                                                if( buf.indexOf( "<body" ) >= 0 && buf.indexOf( "</body>" ) < 0 ) {
    192                                                        buf.append( "</body>" );
    193                                                }
    194
    195        //                                      if( buf.indexOf( "<html" ) >= 0 ) {
    196        //                                              buf.append( "</html>" );
    197        //                                      }
    198                                        }
    199                                        else {
    200                                                // 5.6.7.1 (2013/08/09) デバッグ用にincludeしたファイルを保存しておきます。
    201                                                if( incFiles.length() > 0 ) { incFiles.append( " , " ); }
    202                                                incFiles.append( fname );
    203
    204                                                // 5.6.7.1 (2013/08/09) includeしたファイルをキャッシュから検索します。
    205                                                String fileData = INCLUDE_FILES.get( fname );   // キャッシュを検索(fname がキー)
    206                                                if( fileData == null ) {
    207                                                        // ちょっと小細工
    208                                                        String fname2 = fname ;
    209                                                        // include するファイルは、/jsp/ からの絶対パス。
    210                                                        // jsp 直下の場合は、./ 、それ以外は、../ と置き換えます。
    211                                                        if( isUnder ) { fname2 = fname2.replace( "/jsp/","./" ); }
    212                                                        else              { fname2 = fname2.replace( "/jsp/","../" ); }
    213                                                        // 5.6.7.1 (2013/08/09) includeファイルが存在しない場合は、gf共有から取得する。(jar圧縮対応)
    214                                                        File newfile = new File( parentFile,fname2 );
    215                                                        if( !newfile.exists() ) {
    216                                                                if( fname2.contains( "/common/" ) || fname2.contains( "/menu/" ) ) {
    217                                                                        if( realPath == null ) {
    218                                                                                // 本当は classPathから、取得すべき。
    219                                                                                // 今は、実行環境の相対パスの位置に、gf/jsp/common,menu のファイルが必要。
    220                                                                                fname2 = isUnder
    221                                                                                                        ?       "./../../gf/jsp/"  + fname2.substring( 2 )
    222                                                                                                        :       "../../../gf/jsp/" + fname2.substring( 3 ) ;
    223                                                                                newfile = new File( parentFile,fname2 );                // ここでなければ、エラーになる。
    224                                                                        }
    225                                                                        else {
    226                                                                                // 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
    227                                                                                newfile = new File( realPath,fname );   // 稼働している gf の common 等を使用します。
    228                                                                        }
    229                                                                }
    230                                                        }
    231                                                        fileData = getString( newfile,encode );
    232
    233                                                        // 5.6.7.4 (2013/08/30) includeファイルの先頭のpageEncoding指定のチェック用 span タグの出力
    234                                                        // インクルードファイルの先頭には、pageEncoding="UTF-8" 宣言が必要(UTF-8かどうかは未チェック)
    235                                                        if( ! fileData.startsWith( "<jsp:directive.page pageEncoding" ) ) {
    236                                                                // チェック用のspanタグを出力しておきます。
    237                                                                buf.append( "<span type=\"jsp:directive\" pageEncoding=\"non\" file=\"" )
    238                                                                        .append( fname ).append( "\" ><!-- --></span>" ) ;
    239                                                        }
    240                                                        // 6.4.3.2 (2016/02/19) /jsp/*** で始まるファイルのみ、キャッシュします。
    241                                                        // 5.6.7.1 (2013/08/09) includeしたファイルをキャッシュしておきます。
    242                                                        if( fname.startsWith(  "/jsp/" ) ) {
    243                                                                INCLUDE_FILES.put( fname,fileData );                    // includeファイルをキャッシュ(fname がキー)
    244                                                        }
    245                                                }
    246                                                buf.append( fileData );
    247                                        }
    248                                        final int tagout = line.indexOf( "/>",fout+1 );                 // タグの最後 XML なので、このまま。
    249
    250                                        buf.append( line.substring( tagout+2 ) );
    251                                }
    252
    253                                // og:commonForward を見つけた場合は、最後に html タグを出力する。
    254        //                      if( line.indexOf( "<og:commonForward" ) >= 0 ) {
    255        //                              buf.append( "</html>" );
    256        //                      }
    257
    258                                buf.append( CR );
    259                        }
    260                }
    261                catch( final IOException ex ) {
    262                        LogWriter.log( ex );
    263                }
    264                finally {
    265                        Closer.ioClose( reader );
    266                }
    267                return buf.toString();
    268        }
    269
    270        /**
    271         * jspInclude=true 時に、/jsp/common/** 等の include ファイルが存在しない場合の共有取得場所を指定します。
    272         *
    273         * 引数の処理対象ファイル(transformの引数ファイル)が、『.jsp』で、かつ、jspInclude=true の場合、
    274         * そのファイルを INCLUDE するのですが、/jsp/common/** 等の include ファイルは、
    275         * エンジン共通として、jspCommon6.x.x.x.jar で提供しています。
    276         * 従来は、処理対象jspの相対パスで、../../../gf/jsp/commom/** を取り込んでいましたが、
    277         * Tomcat起動フォルダ以外のシステムのJSPチェックなどを行う場合は、gf フォルダが存在しない
    278         * ケースがあります。
    279         * そこで、確実にgf が存在する、処理をキックしている環境の gf を使用するように変更します。
    280         * その環境とは、つまり、エンジン内部変数の REAL_PATH ですが、jsp などが実行していないと取得できません。
    281         *
    282         * @param       path    /jsp/common/** 等の include ファイルの共有取得場所
    283         */
    284        public void setRealPath( final String path ) {
    285                realPath = path ;
    286        }
    287
    288        /**
    289         * インクルードしたファイル名(相対パス)のリスト文字列を返します。
    290         * 通常は、XSLT変換処理でエラーが発生した場合は、includeファイルの整合性が
    291         * おかしい場合が多いので、デバッグ情報として利用します。
    292         * ただし、エラー発生時の位置特定まではできません。
    293         *
    294         * この内部変数は、インスタンス変数ですので、includeファイルのキャッシュとは寿命が異なります。
    295         *
    296         * @og.rev 5.6.7.1 (2013/08/09) 新規追加
    297         *
    298         * @return includeファイル名のリスト文字列
    299         * @og.rtnNotNull
    300         */
    301        public String getIncludeFiles() {
    302                return incFiles.toString();
    303        }
    304
    305        /**
    306         * インクルードしたファイルのキャッシュをクリアします。
    307         * キャッシュは、インスタンスではなく、スタティック変数で管理しています。
    308         * よって、一連の処理の初めと最後にクリアしておいてください。
    309         *
    310         * @og.rev 5.6.7.1 (2013/08/09) 新規追加
    311         */
    312        public static void cacheClear() {
    313                INCLUDE_FILES.clear();
    314        }
    315
    316        /**
    317         * テスト用の main メソッド。
    318         *
    319         * Usage: org.opengion.fukurou.xml.JspIncludeReader inFile [outFile]
    320         *
    321         * @param       args    コマンド引数配列
    322         */
    323        public static void main( final String[] args ) {
    324                final JspIncludeReader reader = new JspIncludeReader();
    325                final String xml = reader.getString( new File( args[0] ),"UTF-8" );
    326
    327                if( args.length > 1 ) {
    328                        final java.io.PrintWriter writer = FileUtil.getPrintWriter( new File( args[1] ),"UTF-8" );
    329                        writer.print( xml );
    330                        Closer.ioClose( writer );
    331                }
    332                else {
    333                        System.out.println( xml );
    334                }
    335        }
    336}