/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.util;

import java.io.IOException;
import java.io.File;

import java.util.List;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.net.URL;

/**
 * このクラスは、指定のディレクトリパスから .class ファイルを検索するクラスです。
 * 検索パスは、実ファイルと、zipファイルの内部、jar ファイルの内部も含みます。
 * 検索結果は、.class を取り除き、ファイルパスを、すべてドット(.)に変換した形式にします。
 * これは、ほとんどクラスのフルパス文字列に相当します。
 * ここで取得されたファイル名より、実クラスオブジェクトの作成が可能になります。
 *
 * このクラスの main メソッドは、クラスパスから指定の名前を持つクラス以下のディレクトリより
 * ファイルを検索します。通常、このクラスの使い方として、取得したクラスファイル名(文字列)
 * から、引数なしコンストラクタを呼び出して、実オブジェクトを生成させるので、通常のフォルダ
 * から検索するより、クラスパス内から検索するペースが多いため、サンプルをそのように設定
 * しています。
 *
 * @og.rev 4.0.0.0 (2004/12/31) 新規作成
 * @og.group 初期化
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class FindClassFiles {
	private final List<String> list = new ArrayList<String>();
	private final int    baseLen;

	private static final String SUFIX     = ".class" ;
	private static final int    SUFIX_LEN = SUFIX.length();

	/**
	 * 検索パスを指定して構築する、コンストラクタです。
	 * ここで見つかったパス以下の classファイル(拡張子は小文字で、.class )を検索します。
	 * このファイル名は ファイルパスを ドット(.)に置き換え、.class を取り除いた格納しておきます。
	 *
	 * ※ Tomcat8.0.3 では、ClassLoader の getResources(String)で取得するURL名が、
	 *    /C:/opengionV6/uap/webapps/gf/WEB-INF/classes/org/opengion/plugin/
	 *    の形式で、最後の "/" を取る為、filepath.length() - 1 処理していましたが、
	 *    Tomcat8.0.5 では、/C:/opengionV6/uap/webapps/gf/WEB-INF/classes/org/opengion/plugin
	 *    の形式で、最後の "/" がなくなっています。
	 *    最後の "/" があってもなくても、new File(String) でディレクトリのオブジェクトを
	 *    作成できるため、filepath.length() に変更します。
	 *
	 * @og.rev 4.0.3.0 (2007/01/07) UNIXパス検索時の、ファイルパスの取得方法の不具合対応
	 * @og.rev 5.0.0.0 (2009/08/03) UNIXパス検索時の、ファイルパスの取得方法の不具合対応
	 * @og.rev 5.0.0.0 (2009/08/03) UNIXパス検索時の、ファイルパスの取得方法の不具合対応
	 * @og.rev 5.7.5.0 (2014/04/04) ファイル名の取得方法の修正
	 *
	 * @param	filepath	対象となるファイル群を検索する、ファイルパス
	 * @param	keyword		検索対象ファイルのキーワード
	 */
	public FindClassFiles( final String filepath,final String keyword ) {
		// jar:file:/実ディレクトリ!先頭クラス または、file:/実ディレクトリ または、/実ディレクトリ

		String dir = filepath;							// 5.5.2.6 (2012/05/25) findbugs対応
		if( filepath.startsWith( "jar:" )
					|| filepath.startsWith( "file:" )
					|| filepath.charAt(0) == '/' ) {		// 4.4.0.0 (2009/08/02)
			int stAdrs = filepath.indexOf( '/' );
			if( filepath.charAt(stAdrs+2) == ':' ) {	// 4.0.3.0 (2007/01/07)
				stAdrs++;
			}
			int edAdrs = filepath.lastIndexOf( '!' );
			if( edAdrs < 0) {
				edAdrs = filepath.length();			// 5.7.5.0 (2014/04/04) 最後の "/" はあってもなくてもよい。
			}
			dir = filepath.substring( stAdrs,edAdrs );
		}

		final File basefile = new File( dir );
		final String baseFilename = basefile.getAbsolutePath() ;
		baseLen = baseFilename.length() - keyword.length();
		findFilename( basefile );
	}

	/**
	 * ファイルパスを ドット(.)に置き換え、.class を取り除いた形式(クラスの完全系)の文字列配列。
	 *
	 * @return	ファイルパスの文字列配列
	 * @og.rtnNotNull
	 */
	public String[] getFilenames() {
		return list.toArray( new String[list.size()] );
	}

	/**
	 * ファイルを再帰的に検索します。
	 * 指定のファイルオブジェクトが ファイルの場合は、.class であればListに追加し、
	 * .zip か .jar では、findJarFiles を呼び出します。
	 * ファイルでない場合は、配列を取り出し、自分自身を再帰的に呼び出します。
	 *
	 * @param	file	ファイル
	 */
	private void findFilename( final File file ) {
		if( file.isFile() ) {
			final String name = file.getAbsolutePath();
			if( name.endsWith( SUFIX ) ) {
				list.add( name.substring( baseLen,name.length()-SUFIX_LEN ).replace( File.separatorChar,'.' ) );
			}
			else if( name.endsWith( ".jar" ) || name.endsWith( ".zip" ) ) {
				findJarFiles( name );
			}
		}
		else {
			final File[] filelist = file.listFiles();
			for( int i=0; i<filelist.length; i++ ) {
				findFilename( filelist[i] );
			}
		}
	}

	/**
	 * jar/zipファイルを検索します。
	 * 圧縮ファイルでは、階層ではなく、Enumeration としてファイルを取り出します。
	 * 拡張子で判断して、Listに追加していきます。
	 *
	 * @og.rev 5.5.2.6 (2012/05/25) JarFile を、Closer#zipClose( ZipFile ) メソッドを利用して、close します。
	 *
	 * @param	filename	ファイル名
	 */
	private void findJarFiles( final String filename ) {
		// 5.5.2.6 (2012/05/25) findbugs対応
		JarFile jarFile = null;
		try {
			jarFile = new JarFile( filename );
			final Enumeration<JarEntry> en = jarFile.entries() ;
			while( en.hasMoreElements() ) {
				final JarEntry ent = en.nextElement();
				if( ! ent.isDirectory() ) {
					final String name = ent.getName();
					if( name.endsWith( SUFIX ) ) {
						list.add( name.substring( 0,name.length()-SUFIX_LEN ).replace( '/','.' ) );
					}
				}
			}
		}
		catch( IOException ex ) {
			final String errMsg = "ファイル読み取りストリームに失敗しました。"
					+ " File=" + filename
					+ ex.getMessage();				// 5.1.8.0 (2010/07/01) errMsg 修正
			throw new RuntimeException( errMsg,ex );
		}
		// 5.5.2.6 (2012/05/25) findbugs対応
		finally {
			Closer.zipClose( jarFile );
		}
	}

	/**
	 * サンプルメイン
	 * ここでは、引数に通常のファイルではなく、クラスパスより取得します。
	 * 通常、取得されたファイル名は、クラスの完全系の文字列なので、クラスパスより取得
	 * している限り、そのまま オブジェクトを構築できることを意味します。
	 *
	 * @param	args	引数
	 */
	public static void main( final String[] args ) {
		try {
			final ClassLoader loader = Thread.currentThread().getContextClassLoader();
			final Enumeration<URL> enume = loader.getResources( args[0] );	// 4.3.3.6 (2008/11/15) Generics警告対応
			while( enume.hasMoreElements() ) {
				final URL url = enume.nextElement();		// 4.3.3.6 (2008/11/15) Generics警告対応
				// jar:file:/実ディレクトリ!先頭クラス または、file:/実ディレクトリ
				System.out.println( "url=" + url.getFile() );

				final FindClassFiles filenames = new FindClassFiles( url.getFile(),args[0] );
				final String[] names = filenames.getFilenames();
				for( int i=0; i<names.length; i++ ) {
					System.out.println( names[i] );
				}
			}
		}
		catch( IOException ex ) {
			final String errMsg = "ファイル読み取りストリームに失敗しました。"
					+ ex.getMessage();
			throw new RuntimeException( errMsg,ex );
		}
	}
}
