iterable) {
+ iterable.forEach((this::add));
+ return this;
+ }
+
}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/ResourceFinder.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/ResourceFinder.java
new file mode 100644
index 000000000..7e95a9040
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/ResourceFinder.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2023. looly(loolly@aliyun.com)
+ * Hutool is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * https://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.hutool.core.io.resource;
+
+import org.dromara.hutool.core.collection.iter.EnumerationIter;
+import org.dromara.hutool.core.compress.ZipUtil;
+import org.dromara.hutool.core.io.IORuntimeException;
+import org.dromara.hutool.core.io.IoUtil;
+import org.dromara.hutool.core.net.url.URLUtil;
+import org.dromara.hutool.core.text.AntPathMatcher;
+import org.dromara.hutool.core.text.CharUtil;
+import org.dromara.hutool.core.text.StrUtil;
+
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipException;
+
+public class ResourceFinder {
+
+ private final ClassLoader classLoader;
+ private final AntPathMatcher pathMatcher;
+
+ /**
+ * 构造
+ *
+ * @param classLoader 类加载器,用于定义查找资源的范围
+ */
+ public ResourceFinder(final ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ this.pathMatcher = new AntPathMatcher();
+ }
+
+ /**
+ * 查找给定表达式对应的资源
+ *
+ * @param locationPattern 路径表达式
+ * @return {@link MultiResource}
+ */
+ public MultiResource find(final String locationPattern) {
+ // 根目录
+ final String rootDirPath = determineRootDir(locationPattern);
+ // 子表达式
+ final String subPattern = locationPattern.substring(rootDirPath.length());
+
+ final MultiResource result = new MultiResource();
+ // 遍历根目录下所有资源,并过滤保留符合条件的资源
+ for (final Resource rootResource : ResourceUtil.getResources(rootDirPath, classLoader)) {
+ if (URLUtil.isJarURL(rootResource.getUrl())) {
+ try {
+ result.addAll(findInJar(rootResource, subPattern));
+ } catch (final IOException e) {
+ throw new IORuntimeException(e);
+ }
+ } else {
+ result.addAll(findInDir(rootResource, subPattern));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 查找jar包中的资源
+ *
+ * @param rootResource 根资源,为jar包文件
+ * @param subPattern 子表达式,如 *.xml
+ * @return 符合条件的资源
+ * @throws IOException IO异常
+ */
+ protected MultiResource findInJar(final Resource rootResource, final String subPattern) throws IOException {
+ final URL rootDirURL = rootResource.getUrl();
+ final URLConnection conn = rootDirURL.openConnection();
+
+ final JarFile jarFile;
+ final String jarFileUrl;
+ String rootEntryPath;
+ final boolean closeJarFile;
+
+ if (conn instanceof JarURLConnection) {
+ final JarURLConnection jarCon = (JarURLConnection) conn;
+ URLUtil.useCachesIfNecessary(jarCon);
+ jarFile = jarCon.getJarFile();
+ final JarEntry jarEntry = jarCon.getJarEntry();
+ rootEntryPath = (jarEntry != null ? jarEntry.getName() : StrUtil.EMPTY);
+ closeJarFile = !jarCon.getUseCaches();
+ } else {
+ //
+ final String urlFile = rootDirURL.getFile();
+ try {
+ int separatorIndex = urlFile.indexOf(URLUtil.WAR_URL_SEPARATOR);
+ if (separatorIndex == -1) {
+ separatorIndex = urlFile.indexOf(URLUtil.JAR_URL_SEPARATOR);
+ }
+ if (separatorIndex != -1) {
+ jarFileUrl = urlFile.substring(0, separatorIndex);
+ rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
+ jarFile = ZipUtil.ofJar(jarFileUrl);
+ } else {
+ jarFile = new JarFile(urlFile);
+ rootEntryPath = StrUtil.EMPTY;
+ }
+ closeJarFile = true;
+ } catch (final ZipException ex) {
+ return new MultiResource();
+ }
+ }
+
+ rootEntryPath = StrUtil.addSuffixIfNot(rootEntryPath, StrUtil.SLASH);
+ // 遍历jar中的entry,筛选之
+ final MultiResource result = new MultiResource();
+
+ try {
+ String entryPath;
+ for (final JarEntry entry : new EnumerationIter<>(jarFile.entries())) {
+ entryPath = entry.getName();
+ if (entryPath.startsWith(rootEntryPath)) {
+ final String relativePath = entryPath.substring(rootEntryPath.length());
+ if (pathMatcher.match(subPattern, relativePath)) {
+ result.add(new UrlResource(URLUtil.getURL(rootDirURL, relativePath)));
+ }
+ }
+ }
+ } finally {
+ if (closeJarFile) {
+ IoUtil.closeQuietly(jarFile);
+ }
+ }
+
+ return result;
+ }
+
+ protected MultiResource findInDir(final Resource rootResource, final String subPattern) {
+
+ }
+
+ /**
+ * 根据给定的路径表达式,找到跟路径
+ * 根路径即不包含表达式的路径,如 "/WEB-INF/*.xml" 返回 "/WEB-INF/"
+ *
+ * @param location 路径表达式
+ */
+ protected String determineRootDir(final String location) {
+ final int prefixEnd = location.indexOf(':') + 1;
+ int rootDirEnd = location.length();
+ while (rootDirEnd > prefixEnd && pathMatcher.isPattern(location.substring(prefixEnd, rootDirEnd))) {
+ rootDirEnd = location.lastIndexOf(CharUtil.SLASH, rootDirEnd - 2) + 1;
+ }
+ if (rootDirEnd == 0) {
+ rootDirEnd = prefixEnd;
+ }
+ return location.substring(0, rootDirEnd);
+ }
+}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/UrlResource.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/UrlResource.java
index 5ad33431a..490aeffed 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/UrlResource.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/UrlResource.java
@@ -12,22 +12,23 @@
package org.dromara.hutool.core.io.resource;
-import org.dromara.hutool.core.io.IORuntimeException;
-import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.file.FileNameUtil;
-import org.dromara.hutool.core.net.NetUtil;
-import org.dromara.hutool.core.util.ObjUtil;
+import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.net.url.URLUtil;
+import org.dromara.hutool.core.util.ObjUtil;
-import java.io.*;
-import java.net.*;
+import java.io.File;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URL;
/**
* URL资源访问类
- * @author Looly
*
+ * @author Looly
*/
-public class UrlResource implements Resource, Serializable{
+public class UrlResource implements Resource, Serializable {
private static final long serialVersionUID = 1L;
protected URL url;
@@ -35,8 +36,10 @@ public class UrlResource implements Resource, Serializable{
protected String name;
//-------------------------------------------------------------------------------------- Constructor start
+
/**
* 构造
+ *
* @param uri URI
* @since 5.7.21
*/
@@ -46,6 +49,7 @@ public class UrlResource implements Resource, Serializable{
/**
* 构造
+ *
* @param url URL
*/
public UrlResource(final URL url) {
@@ -54,12 +58,13 @@ public class UrlResource implements Resource, Serializable{
/**
* 构造
- * @param url URL,允许为空
+ *
+ * @param url URL,允许为空
* @param name 资源名称
*/
public UrlResource(final URL url, final String name) {
this.url = url;
- if(null != url && URLUtil.URL_PROTOCOL_FILE.equals(url.getProtocol())){
+ if (null != url && URLUtil.URL_PROTOCOL_FILE.equals(url.getProtocol())) {
this.lastModified = FileUtil.file(url).lastModified();
}
this.name = ObjUtil.defaultIfNull(name, () -> (null != url ? FileNameUtil.getName(url.getPath()) : null));
@@ -73,7 +78,7 @@ public class UrlResource implements Resource, Serializable{
}
@Override
- public URL getUrl(){
+ public URL getUrl() {
return this.url;
}
@@ -83,8 +88,8 @@ public class UrlResource implements Resource, Serializable{
}
@Override
- public InputStream getStream() throws NoResourceException{
- if(null == this.url){
+ public InputStream getStream() throws NoResourceException {
+ if (null == this.url) {
throw new NoResourceException("Resource URL is null!");
}
return URLUtil.getStream(url);
@@ -98,18 +103,31 @@ public class UrlResource implements Resource, Serializable{
/**
* 获得File
+ *
* @return {@link File}
*/
- public File getFile(){
+ public File getFile() {
return FileUtil.file(this.url);
}
/**
* 返回路径
+ *
* @return 返回URL路径
*/
@Override
public String toString() {
return (null == this.url) ? "null" : this.url.toString();
}
+
+ /**
+ * 获取相对于本资源的资源
+ *
+ * @param relativePath 相对路径
+ * @return 子资源
+ * @since 6.0.0
+ */
+ public UrlResource createRelative(final String relativePath) {
+ return new UrlResource(URLUtil.getURL(getUrl(), relativePath));
+ }
}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/VfsResource.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/VfsResource.java
index cead0f4a4..248cc4b0d 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/VfsResource.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/VfsResource.java
@@ -16,6 +16,7 @@ import org.dromara.hutool.core.classloader.ClassLoaderUtil;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.reflect.method.MethodUtil;
+import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
@@ -37,6 +38,7 @@ public class VfsResource implements Resource {
private static final Method VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED;
private static final Method VIRTUAL_FILE_METHOD_TO_URL;
private static final Method VIRTUAL_FILE_METHOD_GET_NAME;
+ private static final Method VIRTUAL_FILE_METHOD_GET_PHYSICAL_FILE;
static {
final Class> virtualFile = ClassLoaderUtil.loadClass(VFS3_PKG + "VirtualFile");
@@ -47,6 +49,7 @@ public class VfsResource implements Resource {
VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED = virtualFile.getMethod("getLastModified");
VIRTUAL_FILE_METHOD_TO_URL = virtualFile.getMethod("toURL");
VIRTUAL_FILE_METHOD_GET_NAME = virtualFile.getMethod("getName");
+ VIRTUAL_FILE_METHOD_GET_PHYSICAL_FILE = virtualFile.getMethod("getPhysicalFile");
} catch (final NoSuchMethodException ex) {
throw new IllegalStateException("Could not detect JBoss VFS infrastructure", ex);
}
@@ -117,4 +120,14 @@ public class VfsResource implements Resource {
return MethodUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_SIZE);
}
+ /**
+ * 获取物理文件对象
+ *
+ * @return 物理文件对象
+ * @since 6.0.0
+ */
+ public File getFile(){
+ return MethodUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_PHYSICAL_FILE);
+ }
+
}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/net/url/URLUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/net/url/URLUtil.java
index 318e14619..760455ba4 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/net/url/URLUtil.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/net/url/URLUtil.java
@@ -20,7 +20,6 @@ import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.resource.ResourceUtil;
import org.dromara.hutool.core.lang.Assert;
-import org.dromara.hutool.core.net.NetUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.CharsetUtil;
@@ -28,17 +27,9 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.JarURLConnection;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
+import java.net.*;
import java.nio.charset.Charset;
import java.util.Map;
-import java.util.jar.JarFile;
/**
* URL(Uniform Resource Locator)统一资源定位符相关工具类
@@ -52,10 +43,11 @@ import java.util.jar.JarFile;
* protocol :// hostname[:port] / path / [:parameters][?query]#fragment
*
*
- * @author xiaoleilu
+ * @author looly
*/
public class URLUtil {
+ // region const
/**
* 针对ClassPath路径的伪协议前缀(兼容Spring): "classpath:"
*/
@@ -108,6 +100,7 @@ public class URLUtil {
* WAR路径及内部文件路径分界符
*/
public static final String WAR_URL_SEPARATOR = "*/";
+ // endregion
/**
* 将{@link URI}转换为{@link URL}
@@ -215,6 +208,8 @@ public class URLUtil {
}
}
+ // region getURL
+
/**
* 获得URL
*
@@ -243,14 +238,34 @@ public class URLUtil {
*
* @param file URL对应的文件对象
* @return URL
- * @throws HutoolException MalformedURLException
+ * @throws IORuntimeException URL格式错误
*/
public static URL getURL(final File file) {
Assert.notNull(file, "File is null !");
try {
return file.toURI().toURL();
} catch (final MalformedURLException e) {
- throw new HutoolException(e, "Error occured when get URL!");
+ throw new IORuntimeException(e, "Error occurred when get URL!");
+ }
+ }
+
+ /**
+ * 获取相对于给定URL的新的URL
+ * 来自:org.springframework.core.io.UrlResource#createRelativeURL
+ *
+ * @param url 基础URL
+ * @param relativePath 相对路径
+ * @return 相对于URL的子路径URL
+ * @throws IORuntimeException URL格式错误
+ * @since 6.0.0
+ */
+ public static URL getURL(final URL url, String relativePath) throws HutoolException {
+ // # 在文件路径中合法,但是在URL中非法,此处转义
+ relativePath = StrUtil.replace(StrUtil.removePrefix(relativePath, StrUtil.SLASH), "#", "%23");
+ try {
+ return new URL(url, relativePath);
+ } catch (final MalformedURLException e) {
+ throw new IORuntimeException(e, "Error occurred when get URL!");
}
}
@@ -259,7 +274,7 @@ public class URLUtil {
*
* @param files URL对应的文件对象
* @return URL
- * @throws HutoolException MalformedURLException
+ * @throws IORuntimeException URL格式错误
*/
public static URL[] getURLs(final File... files) {
final URL[] urls = new URL[files.length];
@@ -268,11 +283,12 @@ public class URLUtil {
urls[i] = files[i].toURI().toURL();
}
} catch (final MalformedURLException e) {
- throw new HutoolException(e, "Error occured when get URL!");
+ throw new IORuntimeException(e, "Error occurred when get URL!");
}
return urls;
}
+ // endregion
/**
* 获取URL中域名部分,只保留URL中的协议(Protocol)、Host,其它为null。
@@ -351,6 +367,8 @@ public class URLUtil {
return (null != path) ? path : url.getPath();
}
+ // region toURI
+
/**
* 转URL为URI
*
@@ -409,6 +427,7 @@ public class URLUtil {
throw new HutoolException(e);
}
}
+ // endregion
/**
* 提供的URL是否为文件
@@ -482,21 +501,7 @@ public class URLUtil {
return IoUtil.toReader(getStream(url), charset);
}
- /**
- * 从URL中获取JarFile
- *
- * @param url URL
- * @return JarFile
- * @since 4.1.5
- */
- public static JarFile getJarFile(final URL url) {
- try {
- final JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
- return urlConnection.getJarFile();
- } catch (final IOException e) {
- throw new IORuntimeException(e);
- }
- }
+ // region normalize
/**
* 标准化URL字符串,包括:
@@ -593,6 +598,7 @@ public class URLUtil {
}
return protocol + domain + StrUtil.emptyIfNull(path) + StrUtil.emptyIfNull(params);
}
+ // endregion
/**
* 将Map形式的Form表单数据转换为Url参数形式
@@ -611,31 +617,7 @@ public class URLUtil {
return UrlQuery.of(paramMap).build(charset);
}
- /**
- * 获取指定URL对应资源的内容长度,对于Http,其长度使用Content-Length头决定。
- *
- * @param url URL
- * @return 内容长度,未知返回-1
- * @throws IORuntimeException IO异常
- * @since 5.3.4
- */
- public static long getContentLength(final URL url) throws IORuntimeException {
- if (null == url) {
- return -1;
- }
-
- URLConnection conn = null;
- try {
- conn = url.openConnection();
- return conn.getContentLengthLong();
- } catch (final IOException e) {
- throw new IORuntimeException(e);
- } finally {
- if (conn instanceof HttpURLConnection) {
- ((HttpURLConnection) conn).disconnect();
- }
- }
- }
+ // region getDataUri
/**
* Data URI Scheme封装,数据格式为Base64。data URI scheme 允许我们使用内联(inline-code)的方式在网页中包含数据,
@@ -708,6 +690,7 @@ public class URLUtil {
return builder.toString();
}
+ // endregion
/**
* 获取URL对应数据长度
@@ -732,16 +715,21 @@ public class URLUtil {
} else {
// 如果资源打在jar包中或来自网络,使用网络请求长度
// issue#3226, 来自Spring的AbstractFileResolvingResource
+ URLConnection conn = null;
try {
- final URLConnection con = url.openConnection();
- useCachesIfNecessary(con);
- if (con instanceof HttpURLConnection) {
- final HttpURLConnection httpCon = (HttpURLConnection) con;
+ conn = url.openConnection();
+ useCachesIfNecessary(conn);
+ if (conn instanceof HttpURLConnection) {
+ final HttpURLConnection httpCon = (HttpURLConnection) conn;
httpCon.setRequestMethod("HEAD");
}
- return con.getContentLengthLong();
+ return conn.getContentLengthLong();
} catch (final IOException e) {
throw new IORuntimeException(e);
+ } finally {
+ if (conn instanceof HttpURLConnection) {
+ ((HttpURLConnection) conn).disconnect();
+ }
}
}
}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java b/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java
index 500847c37..d83f94788 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java
@@ -16,14 +16,13 @@ import org.dromara.hutool.core.classloader.ClassLoaderUtil;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.collection.iter.EnumerationIter;
import org.dromara.hutool.core.exception.ExceptionUtil;
-import org.dromara.hutool.core.exception.HutoolException;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.file.FileNameUtil;
+import org.dromara.hutool.core.io.resource.JarResource;
import org.dromara.hutool.core.io.resource.ResourceUtil;
import org.dromara.hutool.core.net.url.URLDecoder;
-import org.dromara.hutool.core.net.url.URLUtil;
-import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.text.CharUtil;
+import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.util.SystemUtil;
@@ -268,7 +267,7 @@ public class ClassScanner implements Serializable {
scanFile(new File(URLDecoder.decode(url.getFile(), this.charset)), null);
break;
case "jar":
- scanJar(URLUtil.getJarFile(url));
+ scanJar(new JarResource(url).getJarFile());
break;
}
}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/AntPathMatcher.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/AntPathMatcher.java
index 6c7be9cde..ced09b940 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/text/AntPathMatcher.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/AntPathMatcher.java
@@ -350,7 +350,7 @@ public class AntPathMatcher {
pos += skipped;
skipped = skipSegment(path, pos, pattDir);
if (skipped < pattDir.length()) {
- return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0))));
+ return (skipped > 0 || (!pattDir.isEmpty() && isWildcardChar(pattDir.charAt(0))));
}
pos += skipped;
}
@@ -657,9 +657,9 @@ public class AntPathMatcher {
/**
- * Tests whether or not a string matches against a pattern via a {@link Pattern}.
+ * Tests whether a string matches against a pattern via a {@link Pattern}.
* The pattern may contain special characters: '*' means zero or more characters; '?' means one and
- * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}.
+ * only one character; '{' and '}' indicate a URI template pattern. For example {@code /users/{user}}.
*/
protected static class AntPathStringMatcher {