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 * 現時点では、 & , < , <= , > , >= を前処理します。
036 *
037 * JSP では、og:head タグで、<html> を出力したり、htmlend.jsp インクルードで
038 * </body></html> を出力していますが、フレームや、フォワードなど、整合性が
039 * 取れないケースがありますので、XML処理用として、<html> を出力していません。
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}