    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         *
    088         * @param       file    JSPファイル
    089         * @param       encode  ファイルのエンコード
    090         *
    091         * @return      インクルードを考慮した、JSPファイル
    092         * @og.rtnNotNull
    093         */
    094        public String getString( final File file,final String encode ) {
    095                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE ) ;
    096                final BufferedReader reader = FileUtil.getBufferedReader( file,encode );
    097
    098                // ファイルが、jsp 直下かどうかを判断します。
    099                final String parentFile = file.getParent() ;
    100                final boolean isUnder = parentFile.endsWith( "\\jsp" );
    101
    102                int  cmntIn    = -1;
    103                int  cmntOut   = -1;
    104                boolean isCmnt = false;
    105                boolean isEscape = true;        // エスケープするかどうか(true:する/false:しない)
    106                try {
    107                        String line ;
    108                        while((line = reader.readLine()) != null) {
    109                                // 5.2.1.0 (2010/10/01) directive.include で、XMLタグは取り込まない。
    110                                if( line.indexOf( "<?xml" ) >= 0 && line.indexOf( "?>" ) >= 0 ) { continue; }
    111                                // jsp:root があれば、エスケープ処理を行わない
    112                                if( line.indexOf( "<jsp:root" ) >= 0 ) { isEscape = false; }
    113
    114                                // コメントの削除
    115                                cmntIn  = line.indexOf( "<!--" );
    116                                cmntOut = line.indexOf( "-->" );
    117                                if( cmntIn >= 0 && cmntOut >= 0 ) {
    118                                        line = line.substring( 0,cmntIn ) + line.substring( cmntOut+3 );        // 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
    119                                }
    120                                else if( cmntIn >= 0 && cmntOut < 0 ) {
    121                                        line = line.substring( 0,cmntIn );
    122                                        isCmnt = true;
    123                                }
    124                                else if( cmntIn < 0  && cmntOut >= 0 ) {
    125                                        line = line.substring( cmntOut+3 );                     // 5.6.7.1 (2013/08/09) コメントの処理のバグ修正
    126                                        isCmnt = false;
    127                                }
    128                                else if( isCmnt && cmntIn < 0 && cmntOut < 0 ) { continue; }
    129
    130                                // 特殊処理：og:head で html タグを出力している。
    131        //                      if( line.indexOf( "<og:head" ) >= 0 ) {
    132        //                              buf.append( "<html>" );
    133        //                      }
    134
    135                                if( isEscape ) {
    136                                        // 5.6.5.2 (2013/06/21) 小細工内容の変更。replaceAll にするのと、スペースまたはタブを使用します。
    137                                        // & , < , <= , > , >= を前処理します。
    138                                        line = line.replaceAll( "&"  ,"&amp;" );                                // ちょっと小細工
    139                                        line = line.replaceAll( "[ \\t]<[ \\t]"," &lt; " );             // ちょっと小細工
    140                                        line = line.replaceAll( "[ \\t]>[ \\t]"," &gt; " );             // ちょっと小細工
    141                                        line = line.replaceAll( "[ \\t]<="," &lt;=" );                  // ちょっと小細工
    142                                        line = line.replaceAll( "[ \\t]>="," &gt;=" );                  // ちょっと小細工
    143        // 5.2.1.0 (2010/10/01) 仮廃止
    144        //                              Matcher mtch = ptn.matcher( line );
    145        //                              int adrs = 0;
    146        //                              StringBuilder buf2 = new StringBuilder();
    147        //                              while( mtch.find(adrs) ) {
    148        //                                      String grp = mtch.group(1);
    149        //                                      String htm = StringUtil.htmlFilter( grp );
    150        //                                      int in = mtch.start(1);
    151        //                                      buf2.append( line.substring( adrs,in ) ).append( htm );
    152        //                                      adrs = mtch.end(1);
    153        //                              }
    154        //                              buf2.append( line.substring( adrs ) );
    155        //                              line = buf2.toString();
    156                                }
    157
    158                                final int st = line.indexOf( "<jsp:directive.include" );
    159                                if( st < 0 ) { buf.append( line ); }    // include が無ければ、そのまま追加
    160                                else {
    161                                        buf.append( line.substring( 0,st ) );
    162                                        final int fin = line.indexOf( '\"',st );                        // ファイルの最初
    163                                        final int fout= line.indexOf( '\"',fin+1 );                     // ファイルの最後
    164                                        final String fname = line.substring( fin+1,fout );      // ファイル名
    165
    166                                        // 5.6.7.2 (2013/08/16) includeファイルを取り込む場合、代わりのspanタグを出力しておきます。
    167                                        buf.append( "<span type=\"jsp:directive\" include=\"" )
    168                                                .append( fname ).append( "\" ><!-- --></span>" ) ;
    169
    170                                        // htmlend.jsp の インクルードは行わない。
    171        //                              if( fname.endsWith( "htmlend.jsp" ) ) {
    172                                        if( fname.contains( "htmlend" ) ) {                                     // 6.3.9.1 (2015/11/27)
    173                                                if( buf.indexOf( "<body" ) >= 0 && buf.indexOf( "</body>" ) < 0 ) {
    174                                                        buf.append( "</body>" );
    175                                                }
    176
    177        //                                      if( buf.indexOf( "<html" ) >= 0 ) {
    178        //                                              buf.append( "</html>" );
    179        //                                      }
    180                                        }
    181                                        else {
    182                                                // 5.6.7.1 (2013/08/09) デバッグ用にincludeしたファイルを保存しておきます。
    183                                                if( incFiles.length() > 0 ) { incFiles.append( " , " ); }
    184                                                incFiles.append( fname );
    185
    186                                                // 5.6.7.1 (2013/08/09) includeしたファイルをキャッシュから検索します。
    187                                                String fileData = INCLUDE_FILES.get( fname );   // キャッシュを検索(fname がキー)
    188                                                if( fileData == null ) {
    189                                                        // ちょっと小細工
    190                                                        String fname2 = fname ;
    191                                                        // include するファイルは、/jsp/ からの絶対パス。
    192                                                        // jsp 直下の場合は、./ 、それ以外は、../ と置き換えます。
    193                                                        if( isUnder ) { fname2 = fname2.replace( "/jsp/","./" ); }
    194                                                        else              { fname2 = fname2.replace( "/jsp/","../" ); }
    195                                                        // 5.6.7.1 (2013/08/09) includeファイルが存在しない場合は、gf共有から取得する。(jar圧縮対応)
    196                                                        File newfile = new File( parentFile,fname2 );
    197                                                        if( !newfile.exists() ) {
    198                                                                if( fname2.contains( "/common/" ) || fname2.contains( "/menu/" ) ) {
    199                                                                        if( realPath == null ) {
    200                                                                                // 本当は classPathから、取得すべき。
    201                                                                                // 今は、実行環境の相対パスの位置に、gf/jsp/common,menu のファイルが必要。
    202                                                                                fname2 = isUnder
    203                                                                                                        ?       "./../../gf/jsp/"  + fname2.substring( 2 )
    204                                                                                                        :       "../../../gf/jsp/" + fname2.substring( 3 ) ;
    205                                                                                newfile = new File( parentFile,fname2 );                // ここでなければ、エラーになる。
    206                                                                        }
    207                                                                        else {
    208                                                                                // 5.7.6.2 (2014/05/16) realPath で、/jsp/common/以下に、実ファイルが存在しない場合の代替取得先を指定します。
    209                                                                                newfile = new File( realPath,fname );   // 稼働している gf の common 等を使用します。
    210                                                                        }
    211                                                                }
    212                                                        }
    213                                                        fileData = getString( newfile,encode );
    214
    215                                                        // 5.6.7.4 (2013/08/30) includeファイルの先頭のpageEncoding指定のチェック用 span タグの出力
    216                                                        // インクルードファイルの先頭には、pageEncoding="UTF-8" 宣言が必要(UTF-8かどうかは未チェック)
    217                                                        if( ! fileData.startsWith( "<jsp:directive.page pageEncoding" ) ) {
    218                                                                // チェック用のspanタグを出力しておきます。
    219                                                                buf.append( "<span type=\"jsp:directive\" pageEncoding=\"non\" file=\"" )
    220                                                                        .append( fname ).append( "\" ><!-- --></span>" ) ;
    221                                                        }
    222                                                        // 6.4.3.2 (2016/02/19) /jsp/*** で始まるファイルのみ、キャッシュします。
    223                                                        // 5.6.7.1 (2013/08/09) includeしたファイルをキャッシュしておきます。
    224                                                        if( fname.startsWith(  "/jsp/" ) ) {
    225                                                                INCLUDE_FILES.put( fname,fileData );                    // includeファイルをキャッシュ(fname がキー)
    226                                                        }
    227                                                }
    228                                                buf.append( fileData );
    229                                        }
    230                                        final int tagout = line.indexOf( "/>",fout+1 ); // タグの最後
    231
    232                                        buf.append( line.substring( tagout+2 ) );
    233                                }
    234
    235                                // og:commonForward を見つけた場合は、最後に html タグを出力する。
    236        //                      if( line.indexOf( "<og:commonForward" ) >= 0 ) {
    237        //                              buf.append( "</html>" );
    238        //                      }
    239
    240                                buf.append( CR );
    241                        }
    242                }
    243                catch( final IOException ex ) {
    244                        LogWriter.log( ex );
    245                }
    246                finally {
    247                        Closer.ioClose( reader );
    248                }
    249                return buf.toString();
    250        }
    251
    252        /**
    253         * jspInclude=true 時に、/jsp/common/** 等の include ファイルが存在しない場合の共有取得場所を指定します。
    254         *
    255         * 引数の処理対象ファイル(transformの引数ファイル)が、『.jsp』で、かつ、jspInclude=true の場合、
    256         * そのファイルを INCLUDE するのですが、/jsp/common/** 等の include ファイルは、
    257         * エンジン共通として、jspCommon6.x.x.x.jar で提供しています。
    258         * 従来は、処理対象jspの相対パスで、../../../gf/jsp/commom/** を取り込んでいましたが、
    259         * Tomcat起動フォルダ以外のシステムのJSPチェックなどを行う場合は、gf フォルダが存在しない
    260         * ケースがあります。
    261         * そこで、確実にgf が存在する、処理をキックしている環境の gf を使用するように変更します。
    262         * その環境とは、つまり、エンジン内部変数の REAL_PATH ですが、jsp などが実行していないと取得できません。
    263         *
    264         * @param       path    /jsp/common/** 等の include ファイルの共有取得場所
    265         */
    266        public void setRealPath( final String path ) {
    267                realPath = path ;
    268        }
    269
    270        /**
    271         * インクルードしたファイル名(相対パス)のリスト文字列を返します。
    272         * 通常は、XSLT変換処理でエラーが発生した場合は、includeファイルの整合性が
    273         * おかしい場合が多いので、デバッグ情報として利用します。
    274         * ただし、エラー発生時の位置特定まではできません。
    275         *
    276         * この内部変数は、インスタンス変数ですので、includeファイルのキャッシュとは寿命が異なります。
    277         *
    278         * @og.rev 5.6.7.1 (2013/08/09) 新規追加
    279         *
    280         * @return includeファイル名のリスト文字列
    281         * @og.rtnNotNull
    282         */
    283        public String getIncludeFiles() {
    284                return incFiles.toString();
    285        }
    286
    287        /**
    288         * インクルードしたファイルのキャッシュをクリアします。
    289         * キャッシュは、インスタンスではなく、スタティック変数で管理しています。
    290         * よって、一連の処理の初めと最後にクリアしておいてください。
    291         *
    292         * @og.rev 5.6.7.1 (2013/08/09) 新規追加
    293         */
    294        public static void cacheClear() {
    295                INCLUDE_FILES.clear();
    296        }
    297
    298        /**
    299         * テスト用の main メソッド。
    300         *
    301         * Usage: org.opengion.fukurou.xml.JspIncludeReader inFile [outFile]
    302         *
    303         * @param       args    コマンド引数配列
    304         */
    305        public static void main( final String[] args ) {
    306                final JspIncludeReader reader = new JspIncludeReader();
    307                final String xml = reader.getString( new File( args[0] ),"UTF-8" );
    308
    309                if( args.length > 1 ) {
    310                        final java.io.PrintWriter writer = FileUtil.getPrintWriter( new File( args[1] ),"UTF-8" );
    311                        writer.print( xml );
    312                        Closer.ioClose( writer );
    313                }
    314                else {
    315                        System.out.println( xml );
    316                }
    317        }
    318}