/*
* Copyright 2004-2012 the Seasar Foundation and the Others.
*
* 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.seasar.util.io;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.seasar.util.jar.JarFileUtil;
import org.seasar.util.lang.ClassLoaderUtil;
import org.seasar.util.lang.ClassUtil;
import org.seasar.util.lang.StringUtil;
import org.seasar.util.log.Logger;
import org.seasar.util.net.URLUtil;
import org.seasar.util.zip.ZipFileUtil;
import org.seasar.util.zip.ZipInputStreamUtil;
import static org.seasar.util.collection.ArrayUtil.*;
import static org.seasar.util.collection.CollectionsUtil.*;
import static org.seasar.util.misc.AssertionUtil.*;
/**
* ファイルシステム上やJarファイル中に展開されている、クラスやリソースの集まりを横断的に処理するためのユーティリティです。
* <p>
* 対象となるファイルシステム上のディレクトリや、Jarファイルの一などは{@link URL}によって与えられます。 URLのプロトコルに応じて適切な
* {@link Traverser}が返されるので、そのメソッドを呼び出すことでクラスやリソースをトラバースすることが出来ます。
* </p>
* <p>
* 次のプロトコルをサポートしています。
* </p>
* <ul>
* <li>{@literal file}</li>
* <li>{@literal jar}</li>
* <li>{@literal wsjar} (WebShpere独自プロトコル、{@literal jar}の別名)</li>
* <li>{@literal zip} (WebLogic独自プロトコル、通常のZipファイルは{@literal jar}プロトコルを使用してください)</li>
* <li>{@literal code-source} (Oracle AS(OC4J)独自プロトコル)</li>
* <li>{@literal vfsfile} (JBossAS5独自プロトコル、{@literal file}の別名)</li>
* <li>{@literal vfszip} (JBossAS5独自プロトコル)</li>
* </ul>
*
* @author koichik
* @see URLUtil#toCanonicalProtocol(String)
* @see ClassTraversalUtil
* @see ResourceTraversalUtil
*/
public abstract class TraversalUtil {
/** 空の{@link Traverser}の配列です。 */
protected static final Traverser[] EMPTY_ARRAY = new Traverser[0];
private static final Logger logger = Logger.getLogger(TraversalUtil.class);
/** URLのプロトコルをキー、{@link TraverserFactory}を値とするマッピングです。 */
protected static final ConcurrentMap<String, TraverserFactory> traverserFactories =
newConcurrentHashMap();
static {
addTraverserFactory("file", new TraverserFactory() {
@Override
public Traverser create(final URL url, final String rootPackage,
final String rootDir) {
return new FileSystemTraverser(
getBaseDir(url, rootDir),
rootPackage,
rootDir);
}
});
addTraverserFactory("jar", new TraverserFactory() {
@Override
public Traverser create(final URL url, final String rootPackage,
final String rootDir) {
return new JarFileTraverser(url, rootPackage, rootDir);
}
});
addTraverserFactory("zip", new TraverserFactory() {
@Override
public Traverser create(final URL url, final String rootPackage,
final String rootDir) {
return new JarFileTraverser(JarFileUtil.create(new File(
ZipFileUtil.toZipFilePath(url))), rootPackage, rootDir);
}
});
addTraverserFactory("code-source", new TraverserFactory() {
@Override
public Traverser create(final URL url, final String rootPackage,
final String rootDir) {
return new JarFileTraverser(URLUtil.create("jar:file:"
+ url.getPath()), rootPackage, rootDir);
}
});
addTraverserFactory("vfszip", new TraverserFactory() {
@Override
public Traverser create(final URL url, final String rootPackage,
final String rootDir) {
return new VfsZipTraverser(url, rootPackage, rootDir);
}
});
}
/**
* {@link TraverserFactory}を追加します。
*
* @param protocol
* URLのプロトコル。{@literal null}や空文字列であってはいけません
* @param factory
* プロトコルに対応する{@link Traverser}のファクトリ。{@literal null}であってはいけません
*/
public static void addTraverserFactory(final String protocol,
final TraverserFactory factory) {
assertArgumentNotEmpty("protocol", protocol);
assertArgumentNotNull("factory", factory);
traverserFactories.put(protocol, factory);
}
/**
* 指定のクラスを基点とする、リソースやクラスの集まりを扱う{@link Traverser}を返します。
* <p>
* このメソッドが返す{@link Traverser}は、指定されたクラスをFQNで参照可能なパスをルートとします。 例えば指定されたクラスが
* <code>foo.Bar</code>で、そのクラスファイルが <code>classes/foo/Bar.class</code>の場合、
* このメソッドが返す {@link Traverser}は<code>classes</code>ディレクトリ以下のリソースの集合を扱います。
* </p>
*
* @param referenceClass
* 基点となるクラス。{@literal null}であってはいけません
* @return 指定のクラスを基点とする、クラスやリソースの集まりを扱う{@link Traverser}
*/
public static Traverser getTraverser(final Class<?> referenceClass) {
assertArgumentNotNull("referenceClass", referenceClass);
final URL url =
ResourceUtil.getResource(toClassFile(referenceClass.getName()));
final String[] path = referenceClass.getName().split("\\.");
String baseUrl = url.toExternalForm();
for (int i = 0; i < path.length; ++i) {
final int pos = baseUrl.lastIndexOf('/');
baseUrl = baseUrl.substring(0, pos);
}
return getTraverser(URLUtil.create(baseUrl + '/'), null, null);
}
/**
* 指定のディレクトリを基点とする、クラスやリソースの集まりを扱う{@link Traverser}を返します。
*
* @param rootDir
* ルートディレクトリ。{@literal null}や空文字列であってはいけません
* @return 指定のディレクトリを基点とする、クラスやリソースの集まりを扱う{@link Traverser}
*/
public static Traverser getTraverser(final String rootDir) {
assertArgumentNotEmpty("rootDir", rootDir);
final URL url =
ResourceUtil.getResource(rootDir.endsWith("/") ? rootDir
: rootDir + '/');
return getTraverser(url, null, rootDir);
}
/**
* 指定のルートパッケージを基点とする、クラスやリソースの集まりを扱う{@link Traverser}の配列を返します。
*
* @param rootPackage
* ルートパッケージ
* @return 指定のルートパッケージを基点とするリソースの集まりを扱う{@link Traverser}の配列
*/
public static Traverser[] getTraversers(final String rootPackage) {
if (StringUtil.isEmpty(rootPackage)) {
return EMPTY_ARRAY;
}
final String baseName = toDirectoryName(rootPackage);
final List<Traverser> list = newArrayList();
for (final Iterator<URL> it = ClassLoaderUtil.getResources(baseName); it
.hasNext();) {
final URL url = it.next();
final Traverser resourcesType =
getTraverser(url, rootPackage, baseName);
if (resourcesType != null) {
list.add(resourcesType);
}
}
if (list.isEmpty()) {
logger.log("WUTL0014", new Object[] { rootPackage });
return EMPTY_ARRAY;
}
return list.toArray(new Traverser[list.size()]);
}
/**
* URLを扱う{@link Traverser}を作成して返します。
* <p>
* URLのプロトコルが未知の場合は<code>null</code>を返します。
* </p>
*
* @param url
* リソースのURL
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
* @return URLを扱う{@link Traverser}
*/
protected static Traverser getTraverser(final URL url,
final String rootPackage, final String rootDir) {
final TraverserFactory factory =
traverserFactories.get(URLUtil.toCanonicalProtocol(url
.getProtocol()));
if (factory != null) {
return factory.create(url, rootPackage, rootDir);
}
logger.log("WUTL0013", asArray(rootPackage, url));
return null;
}
/**
* パッケージ名をディレクトリ名に変換して返します。
*
* @param packageName
* パッケージ名
* @return ディレクトリ名
*/
protected static String toDirectoryName(final String packageName) {
if (StringUtil.isEmpty(packageName)) {
return null;
}
return packageName.replace('.', '/') + '/';
}
/**
* クラス名をクラスファイルのパス名に変換して返します。
*
* @param className
* クラス名
* @return クラスファイルのパス名
*/
protected static String toClassFile(final String className) {
assertArgumentNotNull("className", className);
return className.replace('.', '/') + ".class";
}
/**
* ファイルを表すURLからルートパッケージの上位となるベースディレクトリを求めて返します。
*
* @param url
* ファイルを表すURL
* @param baseName
* ベース名
* @return ルートパッケージの上位となるベースディレクトリ
*/
protected static File getBaseDir(final URL url, final String baseName) {
assertArgumentNotNull("url", url);
File file = URLUtil.toFile(url);
final String[] paths = StringUtil.split(baseName, "/");
for (int i = 0; i < paths.length; ++i) {
file = file.getParentFile();
}
return file;
}
/**
* {@link Traverser}のインスタンスを作成するファクトリです。
*
* @author koichik
*/
public interface TraverserFactory {
/**
* {@link Traverser}のインスタンスを作成して返します。
*
* @param url
* リソースを表すURL
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
* @return URLで表されたリソースを扱う{@link Traverser}
*/
Traverser create(URL url, String rootPackage, String rootDir);
}
/**
* ファイルシステム上のリソースの集まりを扱うオブジェクトです。
*
* @author koichik
*/
public static class FileSystemTraverser implements Traverser {
/** ベースディレクトリです。 */
protected final File baseDir;
/** ルートパッケージです。 */
protected final String rootPackage;
/** ルートディレクトリです。 */
protected final String rootDir;
/**
* インスタンスを構築します。
*
* @param baseDir
* ベースディレクトリ
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
*/
public FileSystemTraverser(final File baseDir,
final String rootPackage, final String rootDir) {
this.baseDir = baseDir;
this.rootPackage = rootPackage;
this.rootDir = rootDir;
}
/**
* インスタンスを構築します。
*
* @param url
* ディレクトリを表すURL
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
*/
public FileSystemTraverser(final URL url, final String rootPackage,
final String rootDir) {
this(URLUtil.toFile(url), rootPackage, rootDir);
}
@Override
public boolean isExistClass(final String className) {
final File file =
new File(baseDir, toClassFile(ClassUtil.concatName(
rootPackage,
className)));
return file.exists();
}
@Override
public void forEach(final ClassHandler handler) {
ClassTraversalUtil.forEach(baseDir, rootPackage, handler);
}
@Override
public void forEach(final ResourceHandler handler) {
ResourceTraversalUtil.forEach(baseDir, rootDir, handler);
}
@Override
public void close() {
}
}
/**
* Jarファイル中のリソースの集まりを扱うオブジェクトです。
*
* @author koichik
*/
public static class JarFileTraverser implements Traverser {
/** Jarファイルです。 */
protected final JarFile jarFile;
/** ルートパッケージです。 */
protected final String rootPackage;
/** ルートディレクトリです。 */
protected final String rootDir;
/**
* インスタンスを構築します。
*
* @param jarFile
* Jarファイル
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
*/
public JarFileTraverser(final JarFile jarFile,
final String rootPackage, final String rootDir) {
this.jarFile = jarFile;
this.rootPackage = rootPackage;
this.rootDir = rootDir;
}
/**
* インスタンスを構築します。
*
* @param url
* Jarファイルを表すURL
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
*/
public JarFileTraverser(final URL url, final String rootPackage,
final String rootDir) {
this(JarFileUtil.toJarFile(url), rootPackage, rootDir);
}
@Override
public boolean isExistClass(final String className) {
return jarFile.getEntry(toClassFile(ClassUtil.concatName(
rootPackage,
className))) != null;
}
@Override
public void forEach(final ClassHandler handler) {
ClassTraversalUtil.forEach(jarFile, new ClassHandler() {
@Override
public void processClass(final String packageName,
final String shortClassName) {
if (rootPackage == null
|| (packageName != null && packageName
.startsWith(rootPackage))) {
handler.processClass(packageName, shortClassName);
}
}
});
}
@Override
public void forEach(final ResourceHandler handler) {
ResourceTraversalUtil.forEach(jarFile, new ResourceHandler() {
@Override
public void processResource(final String path,
final InputStream is) {
if (rootDir == null || path.startsWith(rootDir)) {
handler.processResource(path, is);
}
}
});
}
@Override
public void close() {
JarFileUtil.close(jarFile);
}
}
/**
* JBossAS5のvfszipプロトコルで表されるリソースの集まりを扱うオブジェクトです。
*
* @author koichik
*/
public static class VfsZipTraverser implements Traverser {
/** WAR内の.classファイルの接頭辞です。 */
protected static final String WAR_CLASSES_PREFIX = "/WEB-INF/CLASSES/";
/** ルートパッケージです。 */
protected final String rootPackage;
/** ルートディレクトリです。 */
protected final String rootDir;
/** ZipのURLです。 */
protected final URL zipUrl;
/** Zip内のエントリの接頭辞です。 */
protected final String prefix;
/** Zip内のエントリ名の{@link Set}です。 */
protected final Set<String> entryNames = newHashSet();
/**
* インスタンスを構築します。
*
* @param url
* ルートを表すURL
* @param rootPackage
* ルートパッケージ
* @param rootDir
* ルートディレクトリ
*/
public VfsZipTraverser(final URL url, final String rootPackage,
final String rootDir) {
URL zipUrl = url;
String prefix = "";
if (rootPackage != null) {
final String[] paths = rootPackage.split("\\.");
for (int i = 0; i < paths.length; ++i) {
zipUrl = URLUtil.create(zipUrl, "..");
}
}
loadFromZip(zipUrl);
if (entryNames.isEmpty()) {
final String zipUrlString = zipUrl.toExternalForm();
if (zipUrlString.toUpperCase().endsWith(WAR_CLASSES_PREFIX)) {
final URL warUrl = URLUtil.create(zipUrl, "../..");
final String path = warUrl.getPath();
zipUrl =
FileUtil.toURL(new File(path.substring(
0,
path.length() - 1)));
prefix =
zipUrlString
.substring(warUrl.toExternalForm().length());
loadFromZip(zipUrl);
}
}
this.rootPackage = rootPackage;
this.rootDir = rootDir;
this.zipUrl = zipUrl;
this.prefix = prefix;
}
private void loadFromZip(final URL zipUrl) {
final ZipInputStream zis =
new ZipInputStream(URLUtil.openStream(zipUrl));
try {
ZipEntry entry = null;
while ((entry = ZipInputStreamUtil.getNextEntry(zis)) != null) {
entryNames.add(entry.getName());
ZipInputStreamUtil.closeEntry(zis);
}
} finally {
CloseableUtil.close(zis);
}
}
@Override
public boolean isExistClass(final String className) {
final String entryName =
prefix
+ toClassFile(ClassUtil.concatName(rootPackage, className));
return entryNames.contains(entryName);
}
@Override
public void forEach(final ClassHandler handler) {
final ZipInputStream zis =
new ZipInputStream(URLUtil.openStream(zipUrl));
try {
ClassTraversalUtil.forEach(zis, prefix, new ClassHandler() {
@Override
public void processClass(final String packageName,
final String shortClassName) {
if (rootPackage == null
|| (packageName != null && packageName
.startsWith(rootPackage))) {
handler.processClass(packageName, shortClassName);
}
}
});
} finally {
CloseableUtil.close(zis);
}
}
@Override
public void forEach(final ResourceHandler handler) {
final ZipInputStream zis =
new ZipInputStream(URLUtil.openStream(zipUrl));
try {
ResourceTraversalUtil.forEach(
zis,
prefix,
new ResourceHandler() {
@Override
public void processResource(final String path,
final InputStream is) {
if (rootDir == null || path.startsWith(rootDir)) {
handler.processResource(path, is);
}
}
});
} finally {
CloseableUtil.close(zis);
}
}
@Override
public void close() {
}
}
}