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 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
  • * - * -

    - *

      + *
        *
      1. 香港人在大陆的身份证,【810000】开头;同样可以直接获取到 性别、出生日期
      2. *
      3. 81000019980902013X: 文绎循 男 1998-09-02
      4. *
      5. 810000201011210153: 辛烨 男 2010-11-21
      6. @@ -218,7 +216,6 @@ public class IdcardUtil { *
      7. 830000194609150010: 苏建文 男 1946-09-14
      8. *
      9. 83000019810715006X: 刁婉琇 女 1981-07-15
      10. *
      - *

      * * @param idcard 待验证的身份证 * @return 是否有效的18位身份证,忽略x的大小写 @@ -255,7 +252,7 @@ public class IdcardUtil { *
    1. 通过上面得知如果余数是2,就会在身份证的第18位数字上出现罗马数字的Ⅹ。如果余数是10,身份证的最后一位号码就是2
    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)) {