diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java index 881ec3ad4..ff62b7b8f 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/Response.java @@ -19,6 +19,7 @@ import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.client.body.ResponseBody; import org.dromara.hutool.http.meta.ContentTypeUtil; import org.dromara.hutool.http.meta.HeaderName; +import org.dromara.hutool.http.meta.HttpHeaderUtil; import java.io.Closeable; import java.io.IOException; @@ -92,7 +93,7 @@ public interface Response extends Closeable { * @throws HttpException 包装IO异常 */ default String bodyStr() throws HttpException { - try(final ResponseBody body = body()){ + try (final ResponseBody body = body()) { return body.getString(); } catch (final IOException e) { throw new IORuntimeException(e); @@ -106,7 +107,7 @@ public interface Response extends Closeable { * @return byte[] */ default byte[] bodyBytes() { - try(final ResponseBody body = body()){ + try (final ResponseBody body = body()) { return body.getBytes(); } catch (final IOException e) { throw new IORuntimeException(e); @@ -136,6 +137,16 @@ public interface Response extends Closeable { return header(name.toString()); } + /** + * 根据name获取对应的头信息列表 + * + * @param name Header名 + * @return Header值 + */ + default List headerList(final String name) { + return HttpHeaderUtil.headerList(headers(), name); + } + /** * 获取内容编码 * @@ -172,8 +183,7 @@ public interface Response extends Closeable { * @since 4.6.2 */ default boolean isChunked() { - final String transferEncoding = header(HeaderName.TRANSFER_ENCODING); - return "Chunked".equalsIgnoreCase(transferEncoding); + return "Chunked".equalsIgnoreCase(header(HeaderName.TRANSFER_ENCODING)); } /** @@ -186,6 +196,22 @@ public interface Response extends Closeable { return header(HeaderName.SET_COOKIE); } + /** + * 从Content-Disposition头中获取文件名,以参数名为`filename`为例,规则为: + * + * 按照规范,`Content-Disposition`可能返回多个,此处遍历所有返回头,并且`filename*`始终优先获取,即使`filename`存在并更靠前。
+ * 参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition + * + * @param paramName 文件参数名,如果为{@code null}则使用默认的`filename` + * @return 文件名,empty表示无 + */ + default String getFileNameFromDisposition(final String paramName) { + return HttpHeaderUtil.getFileNameFromDisposition(headers(), paramName); + } + /** * 链式处理结果 * diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java index fb9daa135..d5bc1ff94 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/ResponseBody.java @@ -12,26 +12,20 @@ package org.dromara.hutool.http.client.body; -import org.dromara.hutool.core.io.file.FileUtil; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.StreamProgress; import org.dromara.hutool.core.io.file.FileNameUtil; +import org.dromara.hutool.core.io.file.FileUtil; import org.dromara.hutool.core.io.stream.SyncInputStream; import org.dromara.hutool.core.lang.Assert; -import org.dromara.hutool.core.regex.ReUtil; import org.dromara.hutool.core.text.StrUtil; -import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.HttpException; import org.dromara.hutool.http.HttpGlobalConfig; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.http.html.HtmlUtil; import org.dromara.hutool.http.meta.HeaderName; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; /** * 响应体部分封装 @@ -234,30 +228,11 @@ public class ResponseBody implements HttpBody, Closeable { } // 从头信息中获取文件名 - final String fileName = getFileNameFromDisposition(ObjUtil.defaultIfNull(customParamName, "filename")); + final String fileName = response.getFileNameFromDisposition(customParamName); if (StrUtil.isBlank(fileName)) { throw new HttpException("Can`t get file name from [Content-Disposition]!"); } return FileUtil.file(targetFileOrDir, fileName); } - - /** - * 从Content-Disposition头中获取文件名 - * - * @param paramName 文件名的参数名 - * @return 文件名,empty表示无 - * @since 5.8.10 - */ - private String getFileNameFromDisposition(final String paramName) { - String fileName = null; - final String disposition = response.header(HeaderName.CONTENT_DISPOSITION); - if (StrUtil.isNotBlank(disposition)) { - fileName = ReUtil.get(paramName + "=\"(.*?)\"", disposition, 1); - if (StrUtil.isBlank(fileName)) { - fileName = StrUtil.subAfter(disposition, paramName + "=", true); - } - } - return fileName; - } // endregion ---------------------------------------------------------------------------- Private Methods } diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpHeaderUtil.java b/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpHeaderUtil.java new file mode 100644 index 000000000..760e51abd --- /dev/null +++ b/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpHeaderUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024. 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.http.meta; + +import org.dromara.hutool.core.collection.CollUtil; +import org.dromara.hutool.core.map.CaseInsensitiveMap; +import org.dromara.hutool.core.regex.ReUtil; +import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.util.ObjUtil; + +import java.util.List; +import java.util.Map; + +/** + * HTTP头相关方法 + * + * @author Looly + * @since 6.0.0 + */ +public class HttpHeaderUtil { + + /** + * 根据name获取对应的头信息列表 + * + * @param headers 头列表 + * @param name Header名 + * @return Header值 + */ + public static List headerList(final Map> headers, final String name) { + if (StrUtil.isBlank(name)) { + return null; + } + + final CaseInsensitiveMap> headersIgnoreCase = new CaseInsensitiveMap<>(headers); + return headersIgnoreCase.get(name.trim()); + } + + /** + * 从Content-Disposition头中获取文件名,以参数名为`filename`为例,规则为: + *
    + *
  • 首先按照RFC5987规范检查`filename*`参数对应的值,即:`filename*="example.txt"`,则获取`example.txt`
  • + *
  • 如果找不到`filename*`参数,则检查`filename`参数对应的值,即:`filename="example.txt"`,则获取`example.txt`
  • + *
+ * 按照规范,`Content-Disposition`可能返回多个,此处遍历所有返回头,并且`filename*`始终优先获取,即使`filename`存在并更靠前。
+ * 参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition + * + * @param headers 头列表 + * @param paramName 文件参数名,如果为{@code null}则使用默认的`filename` + * @return 文件名,empty表示无 + */ + public static String getFileNameFromDisposition(final Map> headers, String paramName) { + paramName = ObjUtil.defaultIfNull(paramName, "filename"); + final List dispositions = headerList(headers, HeaderName.CONTENT_DISPOSITION.name()); + String fileName = null; + if (CollUtil.isNotEmpty(dispositions)) { + + // filename* 采用了 RFC 5987 中规定的编码方式,优先读取 + fileName = getFileNameFromDispositions(dispositions, StrUtil.addSuffixIfNot(paramName, "*")); + if ((!StrUtil.endWith(fileName, "*")) && StrUtil.isBlank(fileName)) { + fileName = getFileNameFromDispositions(dispositions, paramName); + } + } + + return fileName; + } + + /** + * 从Content-Disposition头中获取文件名 + * + * @param dispositions Content-Disposition头列表 + * @param paramName 文件参数名 + * @return 文件名,empty表示无 + */ + private static String getFileNameFromDispositions(final List dispositions, String paramName) { + String fileName = null; + for (final String disposition : dispositions) { + fileName = ReUtil.getGroup1(paramName + "=\"(.*?)\"", disposition); + if (StrUtil.isNotBlank(fileName)) { + break; + } + } + return fileName; + } +}