org.dromara.hutool.http.meta.HttpHeaderUtil Maven / Gradle / Ivy
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
*
* 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.dromara.hutool.http.meta;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.map.CaseInsensitiveMap;
import org.dromara.hutool.core.net.url.UrlDecoder;
import org.dromara.hutool.core.net.url.UrlEncoder;
import org.dromara.hutool.core.regex.ReUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.text.split.SplitUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.util.ObjUtil;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
/**
* HTTP头相关方法
* 相关规范见:https://www.rfc-editor.org/rfc/rfc5987
*
* @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头,用于下载文件
* 格式为:
* {@code
* attachment;filename="example.txt";filename*=UTF-8''example.txt
* }
*
* @param fileName 文件名
* @param charset 编码
* @return Content-Disposition头
*/
public static String createAttachmentDisposition(final String fileName, final Charset charset) {
final String encodeText = UrlEncoder.encodeAll(fileName, charset);
return StrUtil.format("attachment;filename=\"{}\";filename*={}''{}", encodeText, charset.name(), encodeText);
}
/**
* 从Content-Disposition头中获取文件名。
* 参考标准:https://datatracker.ietf.org/doc/html/rfc6266#section-4.1
* 以参数名为`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.getValue());
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) {
// 正则转义
paramName = StrUtil.replace(paramName, "*", "\\*");
String fileName = null;
for (final String disposition : dispositions) {
fileName = ReUtil.getGroup1(paramName + "\\s*=\\s*([^;]+)", disposition);
if (StrUtil.isNotBlank(fileName)) {
break;
}
}
return getRfc5987Value(fileName);
}
/**
* 获取rfc5987标准的值,标准见:https://www.rfc-editor.org/rfc/rfc5987#section-3.2.1
* 包括:
*
*
* - Non-extended:无双引号包裹的值
* - Non-extended:双引号包裹的值
* - Extended notation:编码'语言'值
*
*
* @param value 值
* @return 结果值
*/
private static String getRfc5987Value(final String value) {
return ExtendedValue.of(value).getDecodeValue();
}
/**
* 根据rfc5987的扩展值,格式为:
* {@code
* 编码'语言'值
* }
*/
public static class ExtendedValue implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 创建扩展值
*
* @param value 扩展值或普通值
* @return 扩展值
*/
public static ExtendedValue of(final String value) {
return new ExtendedValue(value);
}
private Charset charset;
private String language;
private final String value;
/**
* 构造
*
* @param value 扩展值或普通值
*/
public ExtendedValue(final String value) {
final List split = SplitUtil.split(value, "'");
if (3 == split.size()) {
this.charset = CharsetUtil.charset(split.get(0));
this.language = split.get(1);
this.value = split.get(2);
} else {
this.value = StrUtil.unWrap(value, '"');
;
}
}
/**
* 获取编码,无定义为{@code null}
*
* @return 编码,无定义为{@code null}
*/
public Charset getCharset() {
return charset;
}
/**
* 获取语言
*
* @return 语言
*/
public String getLanguage() {
return language;
}
/**
* 获取原始值
*
* @return 原始值
*/
public String getValue() {
return value;
}
/**
* 获取解码后的值
*
* @return 解码后的值
*/
public String getDecodeValue() {
// 非application/x-www-form-urlencoded环境+都不做解码
return UrlDecoder.decodeForPath(value, charset);
}
}
}