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