package org.nutz.resource;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.servlet.ServletContext;
import org.nutz.castor.Castors;
import org.nutz.lang.Encoding;
import org.nutz.lang.Lang;
import org.nutz.lang.Stopwatch;
import org.nutz.lang.Streams;
import org.nutz.lang.util.ClassTools;
import org.nutz.lang.util.Disks;
import org.nutz.lang.util.FileVisitor;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.resource.impl.ErrorResourceLocation;
import org.nutz.resource.impl.FileResource;
import org.nutz.resource.impl.FileSystemResourceLocation;
import org.nutz.resource.impl.JarResource;
import org.nutz.resource.impl.JarResourceLocation;
import org.nutz.resource.impl.ResourceLocation;
import org.nutz.resource.impl.SimpleResource;
import org.nutz.resource.impl.WebClassesResourceLocation;
/**
* 资源扫描的帮助函数集
*
* @author zozoh(zozohtnt@gmail.com)
* @author wendal(wendal1985@gmail.com)
*/
public class Scans {
private static final String FLT_CLASS = "^.+[.]class$";
private static final Log log = Logs.get();
private static Scans me = new Scans();
private Map<String, ResourceLocation> locations = new LinkedHashMap<String, ResourceLocation>();
// 通过/META-INF/MANIFEST.MF等标记文件,获知所有jar文件的路径
protected String[] referPaths = new String[]{ "META-INF/MANIFEST.MF",
"log4j.properties",
".nutz.resource.mark"};
/**
* 在Web环境中使用Nutz的任何功能,都应该先调用这个方法,以初始化资源扫描器
* <p/>
* 调用一次就可以了
*/
public Scans init(final ServletContext sc) {
Stopwatch sw = Stopwatch.begin();
// 获取classes文件夹的路径, 优先级为125
String classesPath = sc.getRealPath("/WEB-INF/classes");
if (classesPath == null)
addResourceLocation(new WebClassesResourceLocation(sc));
else {
ResourceLocation rc = ResourceLocation.file(new File(classesPath));
if (rc instanceof FileSystemResourceLocation)
((FileSystemResourceLocation)rc).priority = 125;
addResourceLocation(rc);
}
// 获取lib文件夹中的全部jar, 优先级是50
Set<String> jars = sc.getResourcePaths("/WEB-INF/lib/");
if (jars != null) {// 这个文件夹不一定存在,尤其是Maven的WebApp项目
for (String path : jars) {
if (!path.endsWith(".jar"))
continue;
try {
addResourceLocation(new JarResourceLocation(sc.getResource(path)));
}
catch (Exception e) {
log.debug("parse jar fail >> " + e.getMessage());
}
}
}
sw.stop();
printLocations(sw);
return this;
}
public List<NutResource> loadResource(String regex, String... paths) {
List<NutResource> list = new LinkedList<NutResource>();
for (String path : paths) {
list.addAll(scan(path, regex));
}
// 如果找不到?
if (list.size() < 1 && paths.length > 0)
throw Lang.makeThrow( RuntimeException.class,
"folder or file like '%s' no found in %s",
regex,
Castors.me().castToString(paths));
return list;
}
public void registerLocation(Class<?> klass) {
if (klass == null)
return;
try {
registerLocation(klass.getProtectionDomain().getCodeSource().getLocation());
}
catch (Throwable e) { // Android上会死
String classFile = klass.getName().replace('.', '/') + ".class";
URL url = ClassTools.getClassLoader().getResource(classFile);
if (url != null) { // 基本上不可能为null
String str = url.toString();
try {
str = URLDecoder.decode(str, Encoding.UTF8);
}
catch (UnsupportedEncodingException e1) {
throw Lang.impossible();
}
str = str.substring(0, str.length() - classFile.length());
try {
registerLocation(new URL(str));
}
catch (Throwable e2) {
if (log.isInfoEnabled())
log.info("Fail to registerLocation --> " + str, e);
}
}
}
}
public void registerLocation(URL url) {
if (url == null)
return;
addResourceLocation(makeResourceLocation(url));
}
protected ResourceLocation makeResourceLocation(URL url) {
try {
String str = url.toString();
if (str.endsWith(".jar")) {
return new JarResourceLocation(url);
} else if (str.contains("jar!")) {
if (str.startsWith("jar:file:")) {
str = str.substring("jar:file:".length());
}
return ResourceLocation.jar(str.substring(0, str.lastIndexOf("jar!") + 3));
} else if (str.startsWith("file:")) {
return ResourceLocation.file(new File(url.getFile()));
} else {
if (str.startsWith("jar:file:"))
return ResourceLocation.jar(str.substring(str.indexOf('!')));
if (log.isDebugEnabled())
log.debug("Unkown URL " + url);
//return ResourceLocation.file(new File(url.toURI()));
}
}
catch (Throwable e) {
if (log.isInfoEnabled())
log.info("Fail to registerLocation --> " + url, e);
}
return ErrorResourceLocation.make(url);
}
public List<NutResource> scan(String src) {
return scan(src, null);
}
/**
* 在磁盘目录或者 CLASSPATH(包括 jar) 中搜索资源
* <p/>
* <b>核心方法</b>
*
* @param src
* 起始路径
* @param regex
* 资源名需要匹配的正则表达式
* @return 资源列表
*/
public List<NutResource> scan(String src, String regex) {
if (src.isEmpty())
throw new RuntimeException("emtry src is NOT allow");
if ("/".equals(src))
throw new RuntimeException("root path is NOT allow");
List<NutResource> list = new ArrayList<NutResource>();
Pattern pattern = regex == null ? null : Pattern.compile(regex);
// 先看看是不是文件系统上一个具体的文件
if (src.startsWith("~/"))
src = Disks.normalize(src);
File srcFile = new File(src);
if (srcFile.exists()) {
if (srcFile.isDirectory()) {
Disks.visitFile(srcFile,
new ResourceFileVisitor(list, src, 250),
new ResourceFileFilter(pattern));
} else {
list.add(new FileResource(src, srcFile).setPriority(250));
}
}
for (ResourceLocation location : locations.values()) {
location.scan(src, pattern, list);
}
// 如果啥都没找到,那么,用增强扫描
if (list.isEmpty()) {
try {
Enumeration<URL> enu = ClassTools.getClassLoader().getResources(src);
if (enu != null && enu.hasMoreElements()) {
while (enu.hasMoreElements()) {
try {
URL url = enu.nextElement();
ResourceLocation loc = makeResourceLocation(url);
if (url.toString().contains("jar!"))
loc.scan(src, pattern, list);
else
loc.scan("", pattern, list);
}
catch (Throwable e) {
if (log.isTraceEnabled())
log.trace("", e);
}
}
}
}
catch (Throwable e) {
if (log.isDebugEnabled())
log.debug("Fail to run deep scan!", e);
}
// 依然是空?
if (list.isEmpty()) {
try {
InputStream ins = getClass().getClassLoader().getResourceAsStream(src);
if (ins != null) {
list.add(new SimpleResource(src, src, ins));
}
}
catch (Exception e) {
}
}
}
List<NutResource> _list = new ArrayList<NutResource>();
OUT: for (NutResource nr : list) {
Iterator<NutResource> it = _list.iterator();
while (it.hasNext()) {
NutResource nr2 = it.next();
if (nr.equals(nr2)) {
if (nr.priority > nr2.priority) {
it.remove();
} else {
continue OUT;
}
}
}
_list.add(nr);
}
list = _list;
Collections.sort(list);
if (log.isDebugEnabled())
log.debugf("Found %s resource by src( %s ) , regex( %s )", list.size(), src, regex);
return list;
}
public List<Class<?>> scanPackage(Class<?> classZ) {
return scanPackage(classZ.getPackage().getName(), FLT_CLASS);
}
public List<Class<?>> scanPackage(Class<?> classZ, String regex) {
return scanPackage(classZ.getPackage().getName(), regex);
}
/**
* 搜索并返回给定包下所有的类(递归)
*
* @param pkg
* 包名或者包路径
*/
public List<Class<?>> scanPackage(String pkg) {
return scanPackage(pkg, FLT_CLASS);
}
/**
* 搜索给定包下所有的类(递归),并返回所有符合正则式描述的类
*
* @param pkg
* 包名或者包路径
* @param regex
* 正则表达式,请注意你需要匹配的名称为 'xxxx.class' 而不仅仅是类名,从而保证选出的对象都是类文件
*/
public List<Class<?>> scanPackage(String pkg, String regex) {
String packagePath = pkg.replace('.', '/').replace('\\', '/');
if (!packagePath.endsWith("/"))
packagePath += "/";
return rs2class(pkg, scan(packagePath, regex));
}
public static boolean isInJar(File file) {
return isInJar(file.getAbsolutePath());
}
public static boolean isInJar(String filePath) {
return filePath.contains(".jar!");
}
public static NutResource makeJarNutResource(File file) {
return makeJarNutResource(file.getAbsolutePath());
}
public static NutResource makeJarNutResource(String filePath) {
JarEntryInfo jeInfo = new JarEntryInfo(filePath);
try {
ZipInputStream zis = makeZipInputStream(jeInfo.getJarPath());
ZipEntry ens = null;
while (null != (ens = zis.getNextEntry())) {
if (ens.isDirectory())
continue;
if (jeInfo.getEntryName().equals(ens.getName())) {
return makeJarNutResource(jeInfo.getJarPath(), ens.getName(), "");
}
}
}
catch (IOException e) {}
return null;
}
public static NutResource makeJarNutResource( final String jarPath,
final String entryName,
final String base) throws IOException {
NutResource nutResource = new JarResource(jarPath, entryName);
if (entryName.equals(base))
nutResource.setName(entryName);
else
nutResource.setName(entryName.substring(base.length()));
nutResource.setSource(jarPath + ":" + entryName);
return nutResource;
}
public static ZipInputStream makeZipInputStream(String jarPath) throws MalformedURLException,
IOException {
ZipInputStream zis = null;
try {
zis = new ZipInputStream(new FileInputStream(jarPath));
}
catch (IOException e) {
zis = new ZipInputStream(new URL(jarPath).openStream());
}
return zis;
}
public static final Scans me() {
return me;
}
/**
* 将一组 NutResource 转换成 class 对象
*
* @param packagePath
* 包前缀
* @param list
* 列表
* @return 类对象列表
*/
private static List<Class<?>> rs2class(String pkg, List<NutResource> list) {
Set<Class<?>> re = new LinkedHashSet<Class<?>>(list.size());
if (!list.isEmpty()) {
for (NutResource nr : list) {
if (!nr.getName().endsWith(".class") || nr.getName().endsWith("package-info.class")) {
continue;
}
// Class快速载入
String className = pkg + "." + nr.getName().substring(0, nr.getName().length() - 6).replaceAll("[/\\\\]", ".");
try {
Class<?> klass = Lang.loadClass(className);
re.add(klass);
continue;
}
catch (Throwable e) {}
// 失败了? 尝试终极方法,当然了,慢多了
InputStream in = null;
try {
in = nr.getInputStream();
className = ClassTools.getClassName(in);
if (className == null) {
if (log.isInfoEnabled())
log.infof("Resource can't map to Class, Resource %s", nr);
continue;
}
Class<?> klass = Lang.loadClass(className);
re.add(klass);
}
catch (Throwable e) {
if (log.isInfoEnabled())
log.info("Resource can't map to Class, Resource " + nr.getName());
}
finally {
Streams.safeClose(in);
}
}
}
return new ArrayList<Class<?>>(re);
}
public static class ResourceFileFilter implements FileFilter {
public boolean accept(File f) {
if (f.isDirectory()) {
String fnm = f.getName().toLowerCase();
// 忽略 SVN 和 CVS 文件,还有Git文件
if (".svn".equals(fnm) || ".cvs".equals(fnm) || ".git".equals(fnm))
return false;
return true;
}
if (f.isHidden())
return false;
return pattern == null || pattern.matcher(f.getName()).find();
}
private Pattern pattern;
public ResourceFileFilter(Pattern pattern) {
super();
this.pattern = pattern;
}
}
public static class ResourceFileVisitor implements FileVisitor {
public void visit(File f) {
list.add(new FileResource(base, f).setPriority(priority));
}
String base;
List<NutResource> list;
int priority;
public ResourceFileVisitor(List<NutResource> list, String base, int priority) {
super();
this.list = list;
this.base = base;
this.priority = priority;
}
}
protected Scans() {
if (Lang.isAndroid) {
if (log.isInfoEnabled())
log.info("Running in Android , so nothing I can scan , just disable myself");
return;
}
Stopwatch sw = Stopwatch.begin();
// 当前文件夹
try {
FileSystemResourceLocation rc = new FileSystemResourceLocation(new File(".").getAbsoluteFile().getCanonicalFile());
rc.priority = 200;
addResourceLocation(rc);
} catch (Throwable e) {
}
// 推测一下nutz自身所在的位置
//registerLocation(Nutz.class);
ClassLoader cloader = ClassTools.getClassLoader();
for (String referPath : referPaths) {
try {
Enumeration<URL> urls = cloader.getResources(referPath);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
String url_str = url.toString();
if (url_str.contains("jar!")) {
String tmp = url_str.substring(0, url_str.lastIndexOf("jar!") + 3);
if (tmp.startsWith("jar:"))
tmp = tmp.substring("jar:".length());
if (tmp.startsWith("file:/"))
tmp = tmp.substring("file:/".length());
if (tmp.contains("tomcat"))
continue;
if (tmp.contains("Java"))
continue;
//jars.add(tmp);
}
else
registerLocation(new URL(url_str.substring(0, url_str.length() - referPath.length())));
}
}
catch (IOException e) {}
}
// 把ClassPath也扫描一下
try {
String classpath = System.getProperties().getProperty("java.class.path");
String[] paths = classpath.split(System.getProperties().getProperty("path.separator"));
for (String pathZ : paths) {
if (pathZ.endsWith(".jar"))
addResourceLocation(ResourceLocation.jar(pathZ));
else
addResourceLocation(ResourceLocation.file(new File(pathZ)));
}
}
catch (Throwable e) {
}
sw.stop();
printLocations(sw);
}
public void addResourceLocation(ResourceLocation loc) {
locations.put(loc.id(), loc);
}
protected void printLocations(Stopwatch sw) {
if (log.isDebugEnabled()) {
log.debugf("Locations count=%d time use %sms", locations.size(), sw.du());
}
if (log.isTraceEnabled()) {
StringBuilder sb = new StringBuilder();
for (ResourceLocation rc : locations.values()) {
sb.append('\t').append(rc.toString()).append("\r\n");
}
log.trace("Locations for Scans:\n" + sb);
}
}
}