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