diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8c3b0b9d..a9008a4f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@
-------------------------------------------------------------------------------------------------------------
-# 5.7.17 (2021-11-24)
+# 5.7.17 (2021-11-26)
### 🐣新特性
* 【core 】 增加AsyncUtil(pr#457@Gitee)
@@ -25,11 +25,15 @@
* 【db 】 Db.executeBatch标记一个重载为弃用(issue#I4JIPH@Gitee)
* 【core 】 增加CharSequenceUtil.subPreGbk重载(issue#I4JO2E@Gitee)
* 【core 】 ReflectUtil.getMethod排除桥接方法(pr#1965@Github)
+* 【http 】 completeFileNameFromHeader在使用path为路径时,自动解码(issue#I4K0FS@Gitee)
+* 【core 】 CopyOptions增加override配置(issue#I4JQ1N@Gitee)
+* 【poi 】 SheetRidReader可以获取所有sheet名(issue#I4JA3M@Gitee)
*
### 🐞Bug修复
* 【core 】 修复FileResource构造fileName参数无效问题(issue#1942@Github)
* 【cache 】 修复WeakCache键值强关联导致的无法回收问题(issue#1953@Github)
* 【core 】 修复ZipUtil相对路径父路径获取null问题(issue#1961@Github)
+* 【http 】 修复HttpUtil.normalizeParams未判空导致的问题(issue#1975@Github)
-------------------------------------------------------------------------------------------------------------
diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java b/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java
index 8fc238457..c9ea67472 100644
--- a/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java
+++ b/hutool-core/src/main/java/cn/hutool/core/bean/PropDesc.java
@@ -121,18 +121,19 @@ public class PropDesc {
/**
* 检查属性是否可读(即是否可以通过{@link #getValue(Object)}获取到值)
+ *
* @param checkTransient 是否检查Transient关键字或注解
* @return 是否可读
* @since 5.4.2
*/
- public boolean isReadable(boolean checkTransient){
+ public boolean isReadable(boolean checkTransient) {
// 检查是否有getter方法或是否为public修饰
- if(null == this.getter && false == ModifierUtil.isPublic(this.field)){
+ if (null == this.getter && false == ModifierUtil.isPublic(this.field)) {
return false;
}
// 检查transient关键字和@Transient注解
- if(checkTransient && isTransientForGet()){
+ if (checkTransient && isTransientForGet()) {
return false;
}
@@ -164,7 +165,7 @@ public class PropDesc {
* 首先调用字段对应的Getter方法获取值,如果Getter方法不存在,则判断字段如果为public,则直接获取字段值
*
* @param bean Bean对象
- * @param targetType 返回属性值需要转换的类型,null表示不转换
+ * @param targetType 返回属性值需要转换的类型,null表示不转换
* @param ignoreError 是否忽略错误,包括转换错误和注入错误
* @return this
* @since 5.4.2
@@ -190,18 +191,19 @@ public class PropDesc {
/**
* 检查属性是否可读(即是否可以通过{@link #getValue(Object)}获取到值)
+ *
* @param checkTransient 是否检查Transient关键字或注解
* @return 是否可读
* @since 5.4.2
*/
- public boolean isWritable(boolean checkTransient){
+ public boolean isWritable(boolean checkTransient) {
// 检查是否有getter方法或是否为public修饰
- if(null == this.setter && false == ModifierUtil.isPublic(this.field)){
+ if (null == this.setter && false == ModifierUtil.isPublic(this.field)) {
return false;
}
// 检查transient关键字和@Transient注解
- if(checkTransient && isTransientForSet()){
+ if (checkTransient && isTransientForSet()) {
return false;
}
@@ -239,7 +241,28 @@ public class PropDesc {
* @since 5.4.2
*/
public PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError) {
- if (ignoreNull && null == value) {
+ return setValue(bean, value, ignoreNull, ignoreError, true);
+ }
+
+ /**
+ * 设置属性值,可以自动转换字段类型为目标类型
+ *
+ * @param bean Bean对象
+ * @param value 属性值,可以为任意类型
+ * @param ignoreNull 是否忽略{@code null}值,true表示忽略
+ * @param ignoreError 是否忽略错误,包括转换错误和注入错误
+ * @param override 是否覆盖目标值,如果不覆盖,会先读取bean的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写
+ * @return this
+ * @since 5.7.17
+ */
+ public PropDesc setValue(Object bean, Object value, boolean ignoreNull, boolean ignoreError, boolean override) {
+ if (null == value && ignoreNull) {
+ return this;
+ }
+
+ // issue#I4JQ1N@Gitee
+ // 非覆盖模式下,如果目标值存在,则跳过
+ if (false == override && null != getValue(bean)) {
return this;
}
diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java
index bcfae58a1..ec0a01d22 100644
--- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java
+++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java
@@ -140,13 +140,44 @@ public class BeanCopier implements Copier, Serializable {
* Map转Map
*
* @param source 源Map
- * @param dest 目标Map
+ * @param targetMap 目标Map
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
- private void mapToMap(Map source, Map dest) {
- if (null != dest && null != source) {
- dest.putAll(source);
- }
+ private void mapToMap(Map source, Map targetMap) {
+ source.forEach((key, value)->{
+ final CopyOptions copyOptions = this.copyOptions;
+ final HashSet ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null;
+
+ // issue#I4JQ1N@Gitee
+ // 非覆盖模式下,如果目标值存在,则跳过
+ if(false == copyOptions.override && null != targetMap.get(key)){
+ return;
+ }
+
+ if(key instanceof CharSequence){
+ if (CollUtil.contains(ignoreSet, key)) {
+ // 目标属性值被忽略或值提供者无此key时跳过
+ return;
+ }
+
+ // 对key做映射,映射后为null的忽略之
+ key = copyOptions.editFieldName(copyOptions.getMappedFieldName(key.toString(), false));
+ if(null == key){
+ return;
+ }
+
+ value = copyOptions.editFieldValue(key.toString(), value);
+ }
+
+
+ if ((null == value && copyOptions.ignoreNullValue) || source == value) {
+ // 当允许跳过空时,跳过
+ //值不能为bean本身,防止循环引用,此类也跳过
+ return;
+ }
+
+ targetMap.put(key, value);
+ });
}
/**
@@ -158,11 +189,11 @@ public class BeanCopier implements Copier, Serializable {
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void beanToMap(Object bean, Map targetMap) {
- final HashSet ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null;
final CopyOptions copyOptions = this.copyOptions;
+ final HashSet ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null;
BeanUtil.descForEach(bean.getClass(), (prop)->{
- if(false == prop.isReadable(copyOptions.isTransientSupport())){
+ if(false == prop.isReadable(copyOptions.transientSupport)){
// 忽略的属性跳过之
return;
}
@@ -178,6 +209,12 @@ public class BeanCopier implements Copier, Serializable {
return;
}
+ // issue#I4JQ1N@Gitee
+ // 非覆盖模式下,如果目标值存在,则跳过
+ if(false == copyOptions.override && null != targetMap.get(key)){
+ return;
+ }
+
Object value;
try {
value = prop.getValue(bean);
@@ -230,7 +267,7 @@ public class BeanCopier implements Copier, Serializable {
// 遍历目标bean的所有属性
BeanUtil.descForEach(actualEditable, (prop)->{
- if(false == prop.isWritable(this.copyOptions.isTransientSupport())){
+ if(false == prop.isWritable(this.copyOptions.transientSupport)){
// 字段不可写,跳过之
return;
}
@@ -270,7 +307,7 @@ public class BeanCopier implements Copier, Serializable {
return;
}
- prop.setValue(bean, value, copyOptions.ignoreNullValue, copyOptions.ignoreError);
+ prop.setValue(bean, value, copyOptions.ignoreNullValue, copyOptions.ignoreError, copyOptions.override);
});
}
}
diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java
index b0e35315f..92ab2937a 100644
--- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java
+++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java
@@ -65,7 +65,11 @@ public class CopyOptions implements Serializable {
/**
* 是否支持transient关键字修饰和@Transient注解,如果支持,被修饰的字段或方法对应的字段将被忽略。
*/
- private boolean transientSupport = true;
+ protected boolean transientSupport = true;
+ /**
+ * 是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写
+ */
+ protected boolean override = true;
/**
* 创建拷贝选项
@@ -259,7 +263,9 @@ public class CopyOptions implements Serializable {
*
* @return 是否支持
* @since 5.4.2
+ * @deprecated 无需此方法,内部使用直接调用属性
*/
+ @Deprecated
public boolean isTransientSupport() {
return this.transientSupport;
}
@@ -276,6 +282,18 @@ public class CopyOptions implements Serializable {
return this;
}
+ /**
+ * 设置是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非{@code null}则写,否则忽略。如果覆盖,则不判断直接写
+ *
+ * @param override 是否覆盖目标值
+ * @return this
+ * @since 5.7.17
+ */
+ public CopyOptions setOverride(boolean override) {
+ this.override = override;
+ return this;
+ }
+
/**
* 获得映射后的字段名
* 当非反向,则根据源字段名获取目标字段名,反之根据目标字段名获取源字段名。
diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java
index dde70af56..66d89dc9b 100644
--- a/hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java
@@ -1,7 +1,7 @@
package cn.hutool.core.collection;
/**
- * 集合相关工具类,包括数组,是{@link CollUtil} 的别名工具类类
+ * 集合相关工具类,包括数组,是 {@link CollUtil} 的别名工具类
*
* @author xiaoleilu
* @see CollUtil
diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java b/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java
index f3b23892c..e03ad64cd 100644
--- a/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java
+++ b/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java
@@ -253,8 +253,10 @@ public class Opt {
* String hutool = Opt.ofBlankAble("hutool").mapOrElse(String::toUpperCase, () -> Console.log("yes")).mapOrElse(String::intern, () -> Console.log("Value is not present~")).get();
* }
*
+ * @param map后新的类型
* @param mapper 包裹里的值存在时的操作
* @param emptyAction 包裹里的值不存在时的操作
+ * @return 新的类型的Opt
* @throws NullPointerException 如果包裹里的值存在时,执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null},则抛出{@code NPE}
*/
public Opt mapOrElse(Function super T, ? extends U> mapper, VoidFunc0 emptyAction) {
diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java
index 970de3ffc..4fa1201f3 100644
--- a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java
@@ -154,7 +154,7 @@ public class IdcardUtil {
* @return 是否有效
*/
public static boolean isValidCard(String idCard) {
- if(StrUtil.isBlank(idCard)){
+ if (StrUtil.isBlank(idCard)) {
return false;
}
@@ -201,9 +201,7 @@ public class IdcardUtil {
* 余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字。其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2
* 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2
*
- *
-
- *
+ *
* - 香港人在大陆的身份证,【810000】开头;同样可以直接获取到 性别、出生日期
* - 81000019980902013X: 文绎循 男 1998-09-02
* - 810000201011210153: 辛烨 男 2010-11-21
@@ -218,7 +216,6 @@ public class IdcardUtil {
* - 830000194609150010: 苏建文 男 1946-09-14
* - 83000019810715006X: 刁婉琇 女 1981-07-15
*
- *
*
* @param idcard 待验证的身份证
* @return 是否有效的18位身份证,忽略x的大小写
@@ -255,7 +252,7 @@ public class IdcardUtil {
* 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2
*
*
- * @param idcard 待验证的身份证
+ * @param idcard 待验证的身份证
* @param ignoreCase 是否忽略大小写。{@code true}则忽略X大小写,否则严格匹配大写。
* @return 是否有效的18位身份证
* @since 5.5.7
@@ -576,7 +573,7 @@ public class IdcardUtil {
*/
public static String getProvinceByIdCard(String idcard) {
final String code = getProvinceCodeByIdCard(idcard);
- if(StrUtil.isNotBlank(code)){
+ if (StrUtil.isNotBlank(code)) {
return CITY_CODES.get(code);
}
return null;
@@ -617,7 +614,7 @@ public class IdcardUtil {
* @return {@link Idcard}
* @since 5.4.3
*/
- public static Idcard getIdcardInfo(String idcard){
+ public static Idcard getIdcardInfo(String idcard) {
return new Idcard(idcard);
}
@@ -761,6 +758,7 @@ public class IdcardUtil {
/**
* 获取年龄
+ *
* @return 年龄
*/
public int getAge() {
diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java
new file mode 100644
index 000000000..0c314d18b
--- /dev/null
+++ b/hutool-core/src/test/java/cn/hutool/core/bean/copier/BeanCopierTest.java
@@ -0,0 +1,50 @@
+package cn.hutool.core.bean.copier;
+
+import lombok.Data;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BeanCopierTest {
+
+ /**
+ * 测试在非覆盖模式下,目标对象有值则不覆盖
+ */
+ @Test
+ public void beanToBeanNotOverrideTest() {
+ final A a = new A();
+ a.setValue("123");
+ final B b = new B();
+ b.setValue("abc");
+
+ final BeanCopier copier = BeanCopier.create(a, b, CopyOptions.create().setOverride(false));
+ copier.copy();
+
+ Assert.assertEquals("abc", b.getValue());
+ }
+
+ /**
+ * 测试在覆盖模式下,目标对象值被覆盖
+ */
+ @Test
+ public void beanToBeanOverrideTest() {
+ final A a = new A();
+ a.setValue("123");
+ final B b = new B();
+ b.setValue("abc");
+
+ final BeanCopier copier = BeanCopier.create(a, b, CopyOptions.create());
+ copier.copy();
+
+ Assert.assertEquals("123", b.getValue());
+ }
+
+ @Data
+ private static class A {
+ private String value;
+ }
+
+ @Data
+ private static class B {
+ private String value;
+ }
+}
diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java
index a7755e00e..5a0d71ca8 100644
--- a/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java
@@ -182,6 +182,12 @@ public class ArrayUtilTest {
Assert.assertEquals(9, range[9]);
}
+ @Test(expected = NegativeArraySizeException.class)
+ public void rangeMinTest() {
+ //noinspection ResultOfMethodCallIgnored
+ ArrayUtil.range(0, Integer.MIN_VALUE);
+ }
+
@Test
public void maxTest() {
int max = ArrayUtil.max(1, 2, 13, 4, 5);
diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java
index f64f1ecd7..03ae911da 100644
--- a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java
+++ b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java
@@ -7,7 +7,6 @@ import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
@@ -434,7 +433,10 @@ public class HttpResponse extends HttpBase implements Closeable {
fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
if (StrUtil.isBlank(fileName)) {
// 编码后的路径做为文件名
- fileName = URLUtil.encodeQuery(path, CharsetUtil.CHARSET_UTF_8);
+ fileName = URLUtil.encodeQuery(path, charset);
+ } else {
+ // issue#I4K0FS@Gitee
+ fileName = URLUtil.decode(fileName, charset);
}
}
return FileUtil.file(targetFileOrDir, fileName);
diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java
index 0257cd464..ffe7ed669 100644
--- a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java
+++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java
@@ -539,6 +539,9 @@ public class HttpUtil {
* @since 4.5.2
*/
public static String normalizeParams(String paramPart, Charset charset) {
+ if(StrUtil.isEmpty(paramPart)){
+ return paramPart;
+ }
final StrBuilder builder = StrBuilder.create(paramPart.length() + 16);
final int len = paramPart.length();
String name = null;
diff --git a/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java
index e829d7752..00ad46b87 100644
--- a/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java
+++ b/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java
@@ -302,6 +302,12 @@ public class HttpUtilTest {
Assert.assertEquals("%E5%8F%82%E6%95%B0", encodeResult);
}
+ @Test
+ public void normalizeBlankParamsTest() {
+ String encodeResult = HttpUtil.normalizeParams("", CharsetUtil.CHARSET_UTF_8);
+ Assert.assertEquals("", encodeResult);
+ }
+
@Test
public void getMimeTypeTest() {
String mimeType = HttpUtil.getMimeType("aaa.aaa");
diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java
index e742e8fa1..ee5c5710a 100644
--- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java
+++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java
@@ -202,7 +202,7 @@ public class Excel07SaxReader implements ExcelSaxReader {
}
// sheetIndex需转换为rid
- final SheetRidReader ridReader = new SheetRidReader().read(xssfReader);
+ final SheetRidReader ridReader = SheetRidReader.parse(xssfReader);
if (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX)) {
// name:开头的被认为是sheet名称直接处理
diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetRidReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetRidReader.java
index 76e295f8b..9cfc772b4 100644
--- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetRidReader.java
+++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetRidReader.java
@@ -1,6 +1,7 @@
package cn.hutool.poi.excel.sax;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
@@ -13,6 +14,7 @@ import org.xml.sax.helpers.DefaultHandler;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -32,6 +34,17 @@ import java.util.Map;
*/
public class SheetRidReader extends DefaultHandler {
+ /**
+ * 从{@link XSSFReader}中解析sheet名、sheet id等相关信息
+ *
+ * @param reader {@link XSSFReader}
+ * @return SheetRidReader
+ * @since 5.7.17
+ */
+ public static SheetRidReader parse(XSSFReader reader) {
+ return new SheetRidReader().read(reader);
+ }
+
private final static String TAG_NAME = "sheet";
private final static String RID_ATTR = "r:id";
private final static String SHEET_ID_ATTR = "sheetId";
@@ -137,6 +150,16 @@ public class SheetRidReader extends DefaultHandler {
return null;
}
+ /**
+ * 获取所有sheet名称
+ *
+ * @return sheet名称
+ * @since 5.7.17
+ */
+ public List getSheetNames() {
+ return ListUtil.toList(this.NAME_RID_MAP.keySet());
+ }
+
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if (TAG_NAME.equalsIgnoreCase(localName)) {