Merge branch 'dromara:v5-dev' into v5-dev

This commit is contained in:
lzpeng723 2021-11-18 17:40:56 +08:00 committed by GitHub
commit 01c510c65e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 1781 additions and 501 deletions

View File

@ -3,7 +3,27 @@
-------------------------------------------------------------------------------------------------------------
# 5.7.16 (2021-11-04)
# 5.7.17 (2021-11-16)
### 🐣新特性
* 【core 】 增加AsyncUtilpr#457@Gitee
* 【http 】 增加HttpResourceissue#1943@Github
* 【http 】 增加BytesBody、FormUrlEncodedBody
* 【cron 】 TaskTable.remove增加返回值issue#I4HX3B@Gitee
* 【core 】 Tree增加filter、filterNew、cloneTree、hasChild方法issue#I4HFC6@Gitee
* 【poi 】 增加ColumnSheetReader及ExcelReader.readColumn支持读取某一列
* 【core 】 IdCardUtil.isValidCard不再自动trimissue#I4I04O@Gitee
* 【core 】 IdCardUtil.isValidCard不再自动trimissue#I4I04O@Gitee
* 【core 】 改进TextFinder支持限制结束位置及反向查找模式
* 【core 】 Opt增加部分方法pr#459@Gitee
* 【core 】 增加DefaultCloneablepr#459@Gitee
*
### 🐞Bug修复
* 【core 】 修复FileResource构造fileName参数无效问题issue#1942@Github
-------------------------------------------------------------------------------------------------------------
# 5.7.16 (2021-11-07)
### 🐣新特性
* 【core 】 增加DateTime.toLocalDateTime
@ -26,6 +46,7 @@
* 【core 】 TreeUtil增加walk方法pr#1932@Gitee
* 【crypto 】 SmUtil增加sm3WithSaltpr#454@Gitee
* 【http 】 增加HttpInterceptorissue#I4H1ZV@Gitee
* 【core 】 Opt增加flattedMapissue#I4H1ZV@Gitee
### 🐞Bug修复
* 【core 】 修复UrlBuilder.addPath歧义问题issue#1912@Github
@ -35,6 +56,7 @@
* 【core 】 修复CompilerUtil.getFileManager参数没有使用的问题issue#I4FIO6@Gitee
* 【core 】 修复NetUtil.isInRange的cidr判断问题pr#1917@Github
* 【core 】 修复RegexPool中对URL正则匹配问题issue#I4GRKD@Gitee
* 【core 】 修复UrlQuery对于application/x-www-form-urlencoded问题issue#1931@Github
-------------------------------------------------------------------------------------------------------------

View File

@ -142,18 +142,18 @@ We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
<version>5.7.17</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.7.16'
implementation 'cn.hutool:hutool-all:5.7.17'
```
## 📥Download
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.16/)
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.17/)
> 🔔note:
> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.

View File

@ -142,20 +142,20 @@ Hutool的存在就是为了减少代码搜索成本避免网络上参差不
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
<version>5.7.17</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.7.16'
implementation 'cn.hutool:hutool-all:5.7.17'
```
### 📥下载jar
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.16/)
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.17/)
> 🔔️注意
> Hutool 5.x支持JDK8+对Android平台没有测试不能保证所有工具类或工具方法可用。

View File

@ -1 +1 @@
5.7.16
5.7.17

View File

@ -1 +1 @@
var version = '5.7.16'
var version = '5.7.17'

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-all</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-aop</artifactId>
@ -19,7 +19,7 @@
<properties>
<!-- versions -->
<cglib.version>3.3.0</cglib.version>
<spring.version>5.3.10</spring.version>
<spring.version>5.3.12</spring.version>
</properties>
<dependencies>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-bloomFilter</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-bom</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-cache</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-captcha</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-core</artifactId>

View File

@ -0,0 +1,28 @@
package cn.hutool.core.clone;
import cn.hutool.core.util.ReflectUtil;
/**
* 克隆默认实现接口用于实现返回指定泛型类型的克隆方法
*
* @param <T> 泛型类型
* @since 5.7.17
*/
public interface DefaultCloneable<T> extends java.lang.Cloneable {
/**
* 浅拷贝提供默认的泛型返回值的clone方法
*
* @return obj
*/
default T clone0() {
try {
return ReflectUtil.invoke(this, "clone");
} catch (Exception e) {
throw new CloneRuntimeException(e);
}
}
}

View File

@ -62,8 +62,11 @@ public class PercentCodec implements Serializable {
* 存放安全编码
*/
private final BitSet safeCharacters;
/**
* 是否编码空格为+
* 是否编码空格为+<br>
* 如果为{@code true}则将空格编码为"+"此项只在"application/x-www-form-urlencoded"中使用<br>
* 如果为{@code false}则空格编码为"%20",此项一般用于URL的Query部分RFC3986规范
*/
private boolean encodeSpaceAsPlus = false;
@ -130,7 +133,9 @@ public class PercentCodec implements Serializable {
}
/**
* 是否将空格编码为+
* 是否将空格编码为+<br>
* 如果为{@code true}则将空格编码为"+"此项只在"application/x-www-form-urlencoded"中使用<br>
* 如果为{@code false}则空格编码为"%20",此项一般用于URL的Query部分RFC3986规范
*
* @param encodeSpaceAsPlus 是否将空格编码为+
* @return this
@ -174,6 +179,7 @@ public class PercentCodec implements Serializable {
continue;
}
// 兼容双字节的Unicode符处理如部分emoji
byte[] ba = buf.toByteArray();
for (byte toEncode : ba) {
// Converting each byte in the buffer

View File

@ -179,7 +179,7 @@ public class CollStreamUtil {
Set<K> key = new HashSet<>();
key.addAll(map1.keySet());
key.addAll(map2.keySet());
Map<K, V> map = new HashMap<>();
Map<K, V> map = MapUtil.newHashMap(key.size());
for (K t : key) {
X x = map1.get(t);
Y y = map2.get(t);

View File

@ -693,7 +693,7 @@ public class DateUtil extends CalendarUtil {
* @since 5.7.14
*/
public static DateTime parse(CharSequence dateStr, DateParser parser, boolean lenient) {
return new DateTime(dateStr, parser);
return new DateTime(dateStr, parser, lenient);
}
/**

View File

@ -32,6 +32,7 @@ import java.io.PushbackInputStream;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
@ -622,6 +623,9 @@ public class IoUtil extends NioUtil {
if (in == null) {
throw new IllegalArgumentException("The InputStream must not be null");
}
if(null != clazz){
in.accept(clazz);
}
try {
//noinspection unchecked
return (T) in.readObject();
@ -1331,4 +1335,19 @@ public class IoUtil extends NioUtil {
public static LineIter lineIter(InputStream in, Charset charset) {
return new LineIter(in, charset);
}
/**
* {@link ByteArrayOutputStream} 转换为String
* @param out {@link ByteArrayOutputStream}
* @param charset 编码
* @return 字符串
* @since 5.7.17
*/
public static String toStr(ByteArrayOutputStream out, Charset charset){
try {
return out.toString(charset.name());
} catch (UnsupportedEncodingException e) {
throw new IORuntimeException(e);
}
}
}

View File

@ -3,6 +3,7 @@ package cn.hutool.core.io.resource;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@ -59,7 +60,7 @@ public class CharSequenceResource implements Resource, Serializable {
@Override
public String getName() {
return this.name.toString();
return StrUtil.str(this.name);
}
@Override

View File

@ -1,6 +1,8 @@
package cn.hutool.core.io.resource;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.URLUtil;
import java.io.File;
@ -18,11 +20,21 @@ public class FileResource implements Resource, Serializable {
private static final long serialVersionUID = 1L;
private final File file;
private final String name;
// ----------------------------------------------------------------------- Constructor start
/**
* 构造
*
* @param path 文件绝对路径或相对ClassPath路径但是这个路径不能指向一个jar包中的文件
*/
public FileResource(String path) {
this(FileUtil.file(path));
}
/**
* 构造文件名使用文件本身的名字带扩展名
*
* @param path 文件
* @since 4.4.1
*/
@ -31,37 +43,31 @@ public class FileResource implements Resource, Serializable {
}
/**
* 构造
* 构造文件名使用文件本身的名字带扩展名
*
* @param file 文件
*/
public FileResource(File file) {
this(file, file.getName());
this(file, null);
}
/**
* 构造
*
* @param file 文件
* @param fileName 文件名如果为null获取文件本身的文件名
* @param fileName 文件名带扩展名如果为null获取文件本身的文件名
*/
public FileResource(File file, String fileName) {
Assert.notNull(file, "File must be not null !");
this.file = file;
this.name = ObjectUtil.defaultIfNull(fileName, file.getName());
}
/**
* 构造
*
* @param path 文件绝对路径或相对ClassPath路径但是这个路径不能指向一个jar包中的文件
*/
public FileResource(String path) {
this(FileUtil.file(path));
}
// ----------------------------------------------------------------------- Constructor end
@Override
public String getName() {
return this.file.getName();
return this.name;
}
@Override
@ -89,6 +95,6 @@ public class FileResource implements Resource, Serializable {
*/
@Override
public String toString() {
return (null == this.file) ? "null" : this.file.toString();
return this.file.toString();
}
}

View File

@ -25,11 +25,11 @@ public class ConsoleTable {
/**
* 表格头信息
*/
private final List<List<String>> HEADER_LIST = new ArrayList<>();
private final List<List<String>> headerList = new ArrayList<>();
/**
* 表格体信息
*/
private final List<List<String>> BODY_LIST = new ArrayList<>();
private final List<List<String>> bodyList = new ArrayList<>();
/**
* 每列最大字符个数
*/
@ -57,7 +57,7 @@ public class ConsoleTable {
}
List<String> l = new ArrayList<>();
fillColumns(l, titles);
HEADER_LIST.add(l);
headerList.add(l);
return this;
}
@ -69,7 +69,7 @@ public class ConsoleTable {
*/
public ConsoleTable addBody(String... values) {
List<String> l = new ArrayList<>();
BODY_LIST.add(l);
bodyList.add(l);
fillColumns(l, values);
return this;
}
@ -101,9 +101,9 @@ public class ConsoleTable {
public String toString() {
StringBuilder sb = new StringBuilder();
fillBorder(sb);
fillRow(sb, HEADER_LIST);
fillRow(sb, headerList);
fillBorder(sb);
fillRow(sb, BODY_LIST);
fillRow(sb, bodyList);
fillBorder(sb);
return sb.toString();
}
@ -158,4 +158,4 @@ public class ConsoleTable {
Console.print(toString());
}
}
}

View File

@ -24,11 +24,14 @@
*/
package cn.hutool.core.lang;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.func.VoidFunc0;
import cn.hutool.core.util.StrUtil;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@ -49,20 +52,6 @@ public class Opt<T> {
*/
private static final Opt<?> EMPTY = new Opt<>(null);
/**
* 包裹里实际的元素
*/
private final T value;
/**
* {@code Opt}的构造函数
*
* @param value 包裹里的元素
*/
private Opt(T value) {
this.value = value;
}
/**
* 返回一个空的{@code Opt}
*
@ -110,6 +99,31 @@ public class Opt<T> {
return StrUtil.isBlankIfStr(value) ? empty() : new Opt<>(value);
}
/**
* 返回一个包裹里{@code List}集合可能为空的{@code Opt}额外判断了集合内元素为空的情况
*
* @param value 传入需要包裹的元素
* @param <T> 包裹里元素的类型
* @return 一个包裹里元素可能为空的 {@code Opt}
*/
public static <T> Opt<List<T>> ofEmptyAble(List<T> value) {
return CollectionUtil.isEmpty(value) ? empty() : new Opt<>(value);
}
/**
* 包裹里实际的元素
*/
private final T value;
/**
* {@code Opt}的构造函数
*
* @param value 包裹里的元素
*/
private Opt(T value) {
this.value = value;
}
/**
* 返回包裹里的元素取不到则为{@code null}注意此处和{@link java.util.Optional#get()}不同的一点是本方法并不会抛出{@code NoSuchElementException}
* 如果元素为空则返回{@code null}如果需要一个绝对不能为{@code null}的值则使用{@link #orElseThrow()}
@ -153,12 +167,14 @@ public class Opt<T> {
* }</pre>
*
* @param action 你想要执行的操作
* @return this
* @throws NullPointerException 如果包裹里的值存在但你传入的操作为{@code null}时抛出
*/
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
public Opt<T> ifPresent(Consumer<? super T> action) {
if (isPresent()) {
action.accept(value);
}
return this;
}
/**
@ -173,14 +189,40 @@ public class Opt<T> {
*
* @param action 包裹里的值存在时的操作
* @param emptyAction 包裹里的值不存在时的操作
* @return this;
* @throws NullPointerException 如果包裹里的值存在时执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null}则抛出{@code NPE}
*/
public void ifPresentOrElse(Consumer<? super T> action, VoidFunc0 emptyAction) {
if (value != null) {
public Opt<T> ifPresentOrElse(Consumer<? super T> action, VoidFunc0 emptyAction) {
if (isPresent()) {
action.accept(value);
} else {
emptyAction.callWithRuntimeException();
}
return this;
}
/**
* 如果包裹里的值存在就执行传入的值存在时的操作({@link Function#apply(Object)})支持链式调用转换为其他类型
* 否则执行传入的值不存在时的操作({@link VoidFunc0}中的{@link VoidFunc0#call()})
*
* <p>
* 如果值存在就转换为大写否则用{@code Console.error}打印另一句字符串
* <pre>{@code
* String hutool = Opt.ofBlankAble("hutool").mapOrElse(String::toUpperCase, () -> Console.log("yes")).mapOrElse(String::intern, () -> Console.log("Value is not present~")).get();
* }</pre>
*
* @param mapper 包裹里的值存在时的操作
* @param emptyAction 包裹里的值不存在时的操作
* @throws NullPointerException 如果包裹里的值存在时执行的操作为 {@code null}, 或者包裹里的值不存在时的操作为 {@code null}则抛出{@code NPE}
*/
public <U> Opt<U> mapOrElse(Function<? super T, ? extends U> mapper, VoidFunc0 emptyAction) {
if (isPresent()) {
return ofNullable(mapper.apply(value));
} else {
emptyAction.callWithRuntimeException();
return empty();
}
}
/**
@ -242,6 +284,28 @@ public class Opt<T> {
}
}
/**
* 如果包裹里的值存在就执行传入的操作({@link Function#apply})并返回该操作返回值
* 如果不存在返回一个空的{@code Opt}
* {@link Opt#map}的区别为 传入的操作返回值必须为 {@link Optional}
*
* @param mapper 值存在时执行的操作
* @param <U> 操作返回值的类型
* @return 如果包裹里的值存在就执行传入的操作({@link Function#apply})并返回该操作返回值
* 如果不存在返回一个空的{@code Opt}
* @throws NullPointerException 如果给定的操作为 {@code null}或者给定的操作执行结果为 {@code null}抛出 {@code NPE}
* @see Optional#flatMap(Function)
* @since 5.7.16
*/
public <U> Opt<U> flattedMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
Objects.requireNonNull(mapper);
if (isEmpty()) {
return empty();
} else {
return ofNullable(mapper.apply(value).orElse(null));
}
}
/**
* 如果包裹里元素的值存在就执行对应的操作并返回本身
* 如果不存在返回一个空的{@code Opt}
@ -395,6 +459,32 @@ public class Opt<T> {
}
}
/**
* 转换为 {@link Optional}对象
*
* @return {@link Optional}对象
* @since 5.7.16
*/
public Optional<T> toOptional() {
return Optional.ofNullable(this.value);
}
/**
* 执行一系列操作如果途中发生 {@code NPE} {@code IndexOutOfBoundsException}返回一个空的{@code Opt}
*
* @param supplier 操作
* @param <T> 类型
* @return 操作执行后的值
*/
public static <T> Opt<T> exec(Supplier<T> supplier) {
try {
return Opt.ofNullable(supplier.get());
} catch (NullPointerException | IndexOutOfBoundsException e) {
return empty();
}
}
/**
* 判断传入参数是否与 {@code Opt}相等
* 在以下情况下返回true

View File

@ -100,6 +100,11 @@ public interface RegexPool {
* 生日
*/
String BIRTHDAY = "^(\\d{2,4})([/\\-.年]?)(\\d{1,2})([/\\-.月]?)(\\d{1,2})日?$";
/**
* URI<br>
* 定义见https://www.ietf.org/rfc/rfc3986.html#appendix-B
*/
String URI = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?";
/**
* URL
*/

View File

@ -2,6 +2,7 @@ package cn.hutool.core.lang.tree;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.ObjectUtil;
@ -21,7 +22,7 @@ import java.util.function.Consumer;
* @author liangbaikai
* @since 5.2.1
*/
public class Tree<T> extends LinkedHashMap<String, Object> implements Node<T> {
public class Tree<T> extends LinkedHashMap<String, Object> implements Node<T> {
private static final long serialVersionUID = 1L;
private final TreeNodeConfig treeNodeConfig;
@ -175,6 +176,16 @@ import java.util.function.Consumer;
return (List<Tree<T>>) this.get(treeNodeConfig.getChildrenKey());
}
/**
* 是否有子节点无子节点则此为叶子节点
*
* @return 是否有子节点
* @since 5.7.17
*/
public boolean hasChild() {
return CollUtil.isNotEmpty(getChildren());
}
/**
* 递归树并处理子树下的节点
*
@ -184,18 +195,67 @@ import java.util.function.Consumer;
public void walk(Consumer<Tree<T>> consumer) {
consumer.accept(this);
final List<Tree<T>> children = getChildren();
if(CollUtil.isNotEmpty(children)){
children.forEach((tree)-> tree.walk(consumer));
if (CollUtil.isNotEmpty(children)) {
children.forEach((tree) -> tree.walk(consumer));
}
}
/**
* 递归过滤并生成新的树<br>
* 通过{@link Filter}指定的过滤规则本节点或子节点满足过滤条件则保留当前节点否则抛弃节点及其子节点
*
* @param filter 节点过滤规则函数只需处理本级节点本身即可
* @return 过滤后的节点{@code null} 表示不满足过滤要求丢弃之
* @see #filter(Filter)
* @since 5.7.17
*/
public Tree<T> filterNew(Filter<Tree<T>> filter) {
return cloneTree().filter(filter);
}
/**
* 递归过滤当前树注意此方法会修改当前树<br>
* 通过{@link Filter}指定的过滤规则本节点或子节点满足过滤条件则保留当前节点否则抛弃节点及其子节点
*
* @param filter 节点过滤规则函数只需处理本级节点本身即可
* @return 过滤后的节点{@code null} 表示不满足过滤要求丢弃之
* @see #filterNew(Filter)
* @since 5.7.17
*/
public Tree<T> filter(Filter<Tree<T>> filter) {
final List<Tree<T>> children = getChildren();
if (CollUtil.isNotEmpty(children)) {
// 递归过滤子节点
final List<Tree<T>> filteredChildren = new ArrayList<>(children.size());
Tree<T> filteredChild;
for (Tree<T> child : children) {
filteredChild = child.filter(filter);
if (null != filteredChild) {
filteredChildren.add(filteredChild);
}
}
if(CollUtil.isNotEmpty(filteredChildren)){
// 子节点有符合过滤条件的节点则本节点保留
return this.setChildren(filteredChildren);
} else {
this.setChildren(null);
}
}
// 子节点都不符合过滤条件检查本节点
return filter.accept(this) ? this : null;
}
/**
* 设置子节点设置后会覆盖所有原有子节点
*
* @param children 子节点列表
* @param children 子节点列表如果为{@code null}表示移除子节点
* @return this
*/
public Tree<T> setChildren(List<Tree<T>> children) {
if(null == children){
this.remove(treeNodeConfig.getChildrenKey());
}
this.put(treeNodeConfig.getChildrenKey(), children);
return this;
}
@ -241,6 +301,34 @@ import java.util.function.Consumer;
return stringWriter.toString();
}
/**
* 递归克隆当前节点即克隆整个树保留字段值<br>
* 注意此方法只会克隆节点节点属性如果是引用类型不会克隆
*
* @return 新的节点
* @since 5.7.17
*/
public Tree<T> cloneTree() {
final Tree<T> result = ObjectUtil.clone(this);
result.setChildren(cloneChildren());
return result;
}
/**
* 递归复制子节点
*
* @return 新的子节点列表
*/
private List<Tree<T>> cloneChildren() {
final List<Tree<T>> children = getChildren();
if (null == children) {
return null;
}
final List<Tree<T>> newChildren = new ArrayList<>(children.size());
children.forEach((t) -> newChildren.add(t.cloneTree()));
return newChildren;
}
/**
* 打印
*

View File

@ -0,0 +1,19 @@
package cn.hutool.core.net;
import cn.hutool.core.codec.PercentCodec;
/**
* application/x-www-form-urlencoded遵循W3C HTML Form content types规范如空格须转++须被编码<br>
* 规范见https://url.spec.whatwg.org/#urlencoded-serializing
*
* @since 5.7.16
*/
public class FormUrlencoded {
/**
* query中的value默认除"-", "_", ".", "*"外都编码<br>
* 这个类似于JDK提供的{@link java.net.URLEncoder}
*/
public static final PercentCodec ALL = PercentCodec.of(RFC3986.UNRESERVED)
.removeSafe('~').addSafe('*').setEncodeSpaceAsPlus(true);
}

View File

@ -3,7 +3,8 @@ package cn.hutool.core.net;
import cn.hutool.core.codec.PercentCodec;
/**
* rfc3986 : https://www.ietf.org/rfc/rfc3986.html 编码实现
* rfc3986 : https://www.ietf.org/rfc/rfc3986.html 编码实现<br>
* 定义见https://www.ietf.org/rfc/rfc3986.html#appendix-A
*
* @author looly
* @since 5.7.16
@ -13,7 +14,7 @@ public class RFC3986 {
/**
* gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
*/
public static final PercentCodec GEN_DELIMS = PercentCodec.of(":/?#[]&");
public static final PercentCodec GEN_DELIMS = PercentCodec.of(":/?#[]@");
/**
* sub-delims = "!" / "$" / "{@code &}" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
@ -21,12 +22,14 @@ public class RFC3986 {
public static final PercentCodec SUB_DELIMS = PercentCodec.of("!$&'()*+,;=");
/**
* reserved = gen-delims / sub-delims
* reserved = gen-delims / sub-delims<br>
* seehttps://www.ietf.org/rfc/rfc3986.html#section-2.2
*/
public static final PercentCodec RESERVED = GEN_DELIMS.orNew(SUB_DELIMS);
/**
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"<br>
* see: https://www.ietf.org/rfc/rfc3986.html#section-2.3
*/
public static final PercentCodec UNRESERVED = PercentCodec.of(unreservedChars());
@ -36,7 +39,8 @@ public class RFC3986 {
public static final PercentCodec PCHAR = UNRESERVED.orNew(SUB_DELIMS).or(PercentCodec.of(":@"));
/**
* segment = pchar
* segment = pchar<br>
* see: https://www.ietf.org/rfc/rfc3986.html#section-3.3
*/
public static final PercentCodec SEGMENT = PCHAR;
/**
@ -60,15 +64,17 @@ public class RFC3986 {
public static final PercentCodec FRAGMENT = QUERY;
/**
* query中的key
*/
public static final PercentCodec QUERY_PARAM_NAME = PercentCodec.of(QUERY).removeSafe('&').removeSafe('=');
/**
* query中的value
* query中的value<br>
* value不能包含"{@code &}"可以包含 "="
*/
public static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(QUERY).removeSafe('&');
/**
* query中的key<br>
* key不能包含"{@code &}" "="
*/
public static final PercentCodec QUERY_PARAM_NAME = PercentCodec.of(QUERY_PARAM_VALUE).removeSafe('=');
/**
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
*

View File

@ -41,9 +41,10 @@ public class URLDecoder implements Serializable {
}
/**
* 解码
* 解码<br>
* 规则见https://url.spec.whatwg.org/#urlencoded-parsing
* <pre>
* 1. +%20转换为空格 ;
* 1. +%20转换为空格(" ");
* 2. "%xy"转换为文本形式,xy是两位16进制的数值;
* 3. 跳过不符合规范的%形式直接输出
* </pre>
@ -66,10 +67,13 @@ public class URLDecoder implements Serializable {
*
* @param str 包含URL编码后的字符串
* @param isPlusToSpace 是否+转换为空格
* @param charset 编码
* @param charset 编码{@code null}表示不做编码
* @return 解码后的字符串
*/
public static String decode(String str, Charset charset, boolean isPlusToSpace) {
if(null == charset){
return str;
}
return StrUtil.str(decode(StrUtil.bytes(str, charset), isPlusToSpace), charset);
}

View File

@ -7,7 +7,8 @@ import cn.hutool.core.util.StrUtil;
import java.nio.charset.Charset;
/**
* URL编码工具
* URL编码工具<br>
* TODO 在6.x中移除此工具无法很好区分URL编码和www-form编码
*
* @since 5.7.13
* @author looly

View File

@ -13,6 +13,7 @@ import java.util.BitSet;
/**
* URL编码数据内容的类型是 application/x-www-form-urlencoded
* TODO 6.x移除此类使用PercentCodec代替无法很好区分URL编码和www-form编码
*
* <pre>
* 1.字符"a"-"z""A"-"Z""0"-"9"".""-""*""_" 都不会被编码;
@ -21,6 +22,7 @@ import java.util.BitSet;
* </pre>
*
* @author looly
* @see cn.hutool.core.codec.PercentCodec
*/
public class URLEncoder implements Serializable {
private static final long serialVersionUID = 1L;

View File

@ -127,6 +127,9 @@ public class UrlPath {
final StringBuilder builder = new StringBuilder();
for (String segment : segments) {
// 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义
// path的第一部分允许有":"其余部分不允许
// 在此处的Path部分特指host之后的部分即不包含第一部分
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset));
}
if (withEngTag || StrUtil.isEmpty(builder)) {

View File

@ -1,13 +1,15 @@
package cn.hutool.core.net.url;
import cn.hutool.core.codec.PercentCodec;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.FormUrlencoded;
import cn.hutool.core.net.RFC3986;
import cn.hutool.core.net.URLDecoder;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import java.nio.charset.Charset;
import java.util.Iterator;
@ -25,6 +27,10 @@ import java.util.Map;
public class UrlQuery {
private final TableMap<CharSequence, CharSequence> query;
/**
* 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
*/
private final boolean isFormUrlEncoded;
/**
* 构建UrlQuery
@ -36,6 +42,17 @@ public class UrlQuery {
return new UrlQuery(queryMap);
}
/**
* 构建UrlQuery
*
* @param queryMap 初始化的查询键值对
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @return UrlQuery
*/
public static UrlQuery of(Map<? extends CharSequence, ?> queryMap, boolean isFormUrlEncoded) {
return new UrlQuery(queryMap, isFormUrlEncoded);
}
/**
* 构建UrlQuery
*
@ -57,9 +74,21 @@ public class UrlQuery {
* @since 5.5.8
*/
public static UrlQuery of(String queryStr, Charset charset, boolean autoRemovePath) {
final UrlQuery urlQuery = new UrlQuery();
urlQuery.parse(queryStr, charset, autoRemovePath);
return urlQuery;
return of(queryStr, charset, autoRemovePath, false);
}
/**
* 构建UrlQuery
*
* @param queryStr 初始化的查询字符串
* @param charset decode用的编码null表示不做decode
* @param autoRemovePath 是否自动去除path部分{@code true}则自动去除第一个?前的内容
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @return UrlQuery
* @since 5.7.16
*/
public static UrlQuery of(String queryStr, Charset charset, boolean autoRemovePath, boolean isFormUrlEncoded) {
return new UrlQuery(isFormUrlEncoded).parse(queryStr, charset, autoRemovePath);
}
/**
@ -72,15 +101,37 @@ public class UrlQuery {
/**
* 构造
*
* @param queryMap 初始化的查询键值对
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @since 5.7.16
*/
public UrlQuery(boolean isFormUrlEncoded) {
this(null, isFormUrlEncoded);
}
/**
* 构造
*
* @param queryMap 初始化的查询键值对
*/
public UrlQuery(Map<? extends CharSequence, ?> queryMap) {
this(queryMap, false);
}
/**
* 构造
*
* @param queryMap 初始化的查询键值对
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @since 5.7.16
*/
public UrlQuery(Map<? extends CharSequence, ?> queryMap, boolean isFormUrlEncoded) {
if (MapUtil.isNotEmpty(queryMap)) {
query = new TableMap<>(queryMap.size());
addAll(queryMap);
} else {
query = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY);
}
this.isFormUrlEncoded = isFormUrlEncoded;
}
/**
@ -144,6 +195,103 @@ public class UrlQuery {
}
}
return doParse(queryStr, charset);
}
/**
* 获得查询的Map
*
* @return 查询的Map只读
*/
public Map<CharSequence, CharSequence> getQueryMap() {
return MapUtil.unmodifiable(this.query);
}
/**
* 获取查询值
*
* @param key
* @return
*/
public CharSequence get(CharSequence key) {
if (MapUtil.isEmpty(this.query)) {
return null;
}
return this.query.get(key);
}
/**
* 构建URL查询字符串即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式<br>
* 对于{@code null}处理规则如下
* <ul>
* <li>如果key为{@code null}则这个键值对忽略</li>
* <li>如果value为{@code null}只保留key如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
* </ul>
*
* @param charset encode编码null表示不做encode编码
* @return URL查询字符串
*/
public String build(Charset charset) {
if (isFormUrlEncoded) {
return build(FormUrlencoded.ALL, FormUrlencoded.ALL, charset);
}
return build(RFC3986.QUERY_PARAM_NAME, RFC3986.QUERY_PARAM_VALUE, charset);
}
/**
* 构建URL查询字符串即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式<br>
* 对于{@code null}处理规则如下
* <ul>
* <li>如果key为{@code null}则这个键值对忽略</li>
* <li>如果value为{@code null}只保留key如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
* </ul>
*
* @param keyCoder 键值对中键的编码器
* @param valueCoder 键值对中值的编码器
* @param charset encode编码null表示不做encode编码
* @return URL查询字符串
* @since 5.7.16
*/
public String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset) {
if (MapUtil.isEmpty(this.query)) {
return StrUtil.EMPTY;
}
final StringBuilder sb = new StringBuilder();
CharSequence name;
CharSequence value;
for (Map.Entry<CharSequence, CharSequence> entry : this.query) {
name = entry.getKey();
if (null != name) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(keyCoder.encode(name, charset));
value = entry.getValue();
if (null != value) {
sb.append("=").append(valueCoder.encode(value, charset));
}
}
}
return sb.toString();
}
@Override
public String toString() {
return build(null);
}
/**
* 解析URL中的查询字符串<br>
* 规则见https://url.spec.whatwg.org/#urlencoded-parsing
*
* @param queryStr 查询字符串类似于key1=v1&amp;key2=&amp;key3=v3
* @param charset decode编码null表示不做decode
* @return this
* @since 5.5.8
*/
private UrlQuery doParse(String queryStr, Charset charset) {
final int len = queryStr.length();
String name = null;
int pos = 0; // 未处理字符开始位置
@ -188,68 +336,6 @@ public class UrlQuery {
return this;
}
/**
* 获得查询的Map
*
* @return 查询的Map只读
*/
public Map<CharSequence, CharSequence> getQueryMap() {
return MapUtil.unmodifiable(this.query);
}
/**
* 获取查询值
*
* @param key
* @return
*/
public CharSequence get(CharSequence key) {
if (MapUtil.isEmpty(this.query)) {
return null;
}
return this.query.get(key);
}
/**
* 构建URL查询字符串即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式<br>
* 对于{@code null}处理规则如下
* <ul>
* <li>如果key为{@code null}则这个键值对忽略</li>
* <li>如果value为{@code null}只保留key如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
* </ul>
*
* @param charset encode编码null表示不做encode编码
* @return URL查询字符串
*/
public String build(Charset charset) {
if (MapUtil.isEmpty(this.query)) {
return StrUtil.EMPTY;
}
final StringBuilder sb = new StringBuilder();
CharSequence name;
CharSequence value;
for (Map.Entry<CharSequence, CharSequence> entry : this.query) {
name = entry.getKey();
if (null != name) {
if(sb.length() >0){
sb.append("&");
}
sb.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset));
value = entry.getValue();
if (null != value) {
sb.append("=").append(RFC3986.QUERY_PARAM_VALUE.encode(value, charset));
}
}
}
return sb.toString();
}
@Override
public String toString() {
return build(null);
}
/**
* 对象转换为字符串用于URL的Query中
*
@ -283,11 +369,11 @@ public class UrlQuery {
*/
private void addParam(String key, String value, Charset charset) {
if (null != key) {
final String actualKey = URLUtil.decode(key, charset);
this.query.put(actualKey, StrUtil.nullToEmpty(URLUtil.decode(value, charset)));
final String actualKey = URLDecoder.decode(key, charset, isFormUrlEncoded);
this.query.put(actualKey, StrUtil.nullToEmpty(URLDecoder.decode(value, charset, isFormUrlEncoded)));
} else if (null != value) {
// name为空value作为namevalue赋值null
this.query.put(URLUtil.decode(value, charset), null);
this.query.put(URLDecoder.decode(value, charset, isFormUrlEncoded), null);
}
}
}

View File

@ -7,6 +7,9 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.Matcher;
import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.text.finder.CharFinder;
import cn.hutool.core.text.finder.Finder;
import cn.hutool.core.text.finder.StrFinder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.CharsetUtil;
@ -35,7 +38,7 @@ import java.util.function.Predicate;
*/
public class CharSequenceUtil {
public static final int INDEX_NOT_FOUND = -1;
public static final int INDEX_NOT_FOUND = Finder.INDEX_NOT_FOUND;
/**
* 字符串常量{@code "null"} <br>
@ -1064,7 +1067,7 @@ public class CharSequenceUtil {
* @param searchChar 被查找的字符
* @return 位置
*/
public static int indexOf(final CharSequence str, char searchChar) {
public static int indexOf(CharSequence str, char searchChar) {
return indexOf(str, searchChar, 0);
}
@ -1087,29 +1090,17 @@ public class CharSequenceUtil {
/**
* 指定范围内查找指定字符
*
* @param str 字符串
* @param text 字符串
* @param searchChar 被查找的字符
* @param start 起始位置如果小于0从0开始查找
* @param end 终止位置如果超过str.length()则默认查找到字符串末尾
* @return 位置
*/
public static int indexOf(final CharSequence str, char searchChar, int start, int end) {
if (isEmpty(str)) {
public static int indexOf(CharSequence text, char searchChar, int start, int end) {
if (isEmpty(text)) {
return INDEX_NOT_FOUND;
}
final int len = str.length();
if (start < 0 || start > len) {
start = 0;
}
if (end > len || end < 0) {
end = len;
}
for (int i = start; i < end; i++) {
if (str.charAt(i) == searchChar) {
return i;
}
}
return INDEX_NOT_FOUND;
return new CharFinder(searchChar).setText(text).setEndIndex(end).start(start);
}
/**
@ -1168,40 +1159,22 @@ public class CharSequenceUtil {
/**
* 指定范围内查找字符串
*
* @param str 字符串
* @param searchStr 需要查找位置的字符串
* @param fromIndex 起始位置
* @param text 字符串空则返回-1
* @param searchStr 需要查找位置的字符串空则返回-1
* @param from 起始位置包含
* @param ignoreCase 是否忽略大小写
* @return 位置
* @since 3.2.1
*/
public static int indexOf(final CharSequence str, CharSequence searchStr, int fromIndex, boolean ignoreCase) {
if (str == null || searchStr == null) {
return INDEX_NOT_FOUND;
}
if (fromIndex < 0) {
fromIndex = 0;
}
final int endLimit = str.length() - searchStr.length() + 1;
if (fromIndex > endLimit) {
return INDEX_NOT_FOUND;
}
if (searchStr.length() == 0) {
return fromIndex;
}
if (false == ignoreCase) {
// 不忽略大小写调用JDK方法
return str.toString().indexOf(searchStr.toString(), fromIndex);
}
for (int i = fromIndex; i < endLimit; i++) {
if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) {
return i;
public static int indexOf(CharSequence text, CharSequence searchStr, int from, boolean ignoreCase) {
if (isEmpty(text) || isEmpty(searchStr)) {
if (StrUtil.equals(text, searchStr)) {
return 0;
} else {
return INDEX_NOT_FOUND;
}
}
return INDEX_NOT_FOUND;
return new StrFinder(searchStr, ignoreCase).setText(text).start(from);
}
/**
@ -1212,7 +1185,7 @@ public class CharSequenceUtil {
* @return 位置
* @since 3.2.1
*/
public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) {
public static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr) {
return lastIndexOfIgnoreCase(str, searchStr, str.length());
}
@ -1226,7 +1199,7 @@ public class CharSequenceUtil {
* @return 位置
* @since 3.2.1
*/
public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) {
public static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr, int fromIndex) {
return lastIndexOf(str, searchStr, fromIndex, true);
}
@ -1234,37 +1207,23 @@ public class CharSequenceUtil {
* 指定范围内查找字符串<br>
* fromIndex 为搜索起始位置从后往前计数
*
* @param str 字符串
* @param text 字符串
* @param searchStr 需要查找位置的字符串
* @param fromIndex 起始位置从后往前计数
* @param from 起始位置从后往前计数
* @param ignoreCase 是否忽略大小写
* @return 位置
* @since 3.2.1
*/
public static int lastIndexOf(final CharSequence str, final CharSequence searchStr, int fromIndex, boolean ignoreCase) {
if (str == null || searchStr == null) {
return INDEX_NOT_FOUND;
}
if (fromIndex < 0) {
fromIndex = 0;
}
fromIndex = Math.min(fromIndex, str.length());
if (searchStr.length() == 0) {
return fromIndex;
}
if (false == ignoreCase) {
// 不忽略大小写调用JDK方法
return str.toString().lastIndexOf(searchStr.toString(), fromIndex);
}
for (int i = fromIndex; i >= 0; i--) {
if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) {
return i;
public static int lastIndexOf(CharSequence text, CharSequence searchStr, int from, boolean ignoreCase) {
if (isEmpty(text) || isEmpty(searchStr)) {
if (StrUtil.equals(text, searchStr)) {
return 0;
} else {
return INDEX_NOT_FOUND;
}
}
return INDEX_NOT_FOUND;
return new StrFinder(searchStr, ignoreCase)
.setText(text).setNegative(true).start(from);
}
/**
@ -4063,6 +4022,19 @@ public class CharSequenceUtil {
return NamingCase.toCamelCase(name);
}
/**
* 将连接符方式命名的字符串转换为驼峰式如果转换前的下划线大写方式命名的字符串为空则返回空字符串<br>
* 例如hello_world=helloWorld; hello-world=helloWorld
*
* @param name 转换前的下划线大写方式命名的字符串
* @param symbol 连接符
* @return 转换后的驼峰式命名的字符串
* @see NamingCase#toCamelCase(CharSequence, char)
*/
public static String toCamelCase(CharSequence name, char symbol) {
return NamingCase.toCamelCase(name, symbol);
}
// ------------------------------------------------------------------------ isSurround
/**

View File

@ -58,7 +58,7 @@ public class NamingCase {
}
/**
* 将驼峰式命名的字符串转换为使用符号连接方式如果转换前的驼峰式命名的字符串为空则返回空字符串<br>
* 将驼峰式命名的字符串转换为使用符号连接方式如果转换前的驼峰式命名的字符串为空则返回空字符串
*
* @param str 转换前的驼峰式命名的字符串也可以为符号连接形式
* @param symbol 连接符
@ -150,19 +150,31 @@ public class NamingCase {
* @return 转换后的驼峰式命名的字符串
*/
public static String toCamelCase(CharSequence name) {
return toCamelCase(name, CharUtil.UNDERLINE);
}
/**
* 将连接符方式命名的字符串转换为驼峰式如果转换前的下划线大写方式命名的字符串为空则返回空字符串
*
* @param name 转换前的自定义方式命名的字符串
* @param symbol 连接符
* @return 转换后的驼峰式命名的字符串
* @since 5.7.17
*/
public static String toCamelCase(CharSequence name, char symbol) {
if (null == name) {
return null;
}
final String name2 = name.toString();
if (StrUtil.contains(name2, CharUtil.UNDERLINE)) {
if (StrUtil.contains(name2, symbol)) {
final int length = name2.length();
final StringBuilder sb = new StringBuilder(length);
boolean upperCase = false;
for (int i = 0; i < length; i++) {
char c = name2.charAt(i);
if (c == CharUtil.UNDERLINE) {
if (c == symbol) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
@ -176,4 +188,5 @@ public class NamingCase {
return name2;
}
}
}

View File

@ -35,7 +35,7 @@ public class StrFormatter {
* 如果想输出占位符使用 \\转义即可如果想输出占位符之前的 \ 使用双转义符 \\\\ 即可<br>
* <br>
* 通常使用format("this is {} for {}", "{}", "a", "b") = this is a for b<br>
* 转义{} format("this is \\{} for {}", "{}", "a", "b") = this is \{} for a<br>
* 转义{} format("this is \\{} for {}", "{}", "a", "b") = this is {} for a<br>
* 转义\ format("this is \\\\{} for {}", "{}", "a", "b") = this is \a for b<br>
*
* @param strPattern 字符串模板

View File

@ -169,10 +169,9 @@ public class StrSplitter {
* @param ignoreEmpty 是否忽略空串
* @param ignoreCase 是否忽略大小写
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> split(CharSequence text, char separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) {
return split(text, separator, limit, ignoreEmpty, trimFunc(isTrim));
return split(text, separator, limit, ignoreEmpty, ignoreCase, trimFunc(isTrim));
}
/**

View File

@ -4,7 +4,8 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.NumberUtil;
/**
* 字符查找器
* 字符查找器<br>
* 查找指定字符在字符串中的位置信息
*
* @author looly
* @since 5.7.14
@ -38,10 +39,18 @@ public class CharFinder extends TextFinder {
@Override
public int start(int from) {
Assert.notNull(this.text, "Text to find must be not null!");
final int length = text.length();
for (int i = from; i < length; i++) {
if (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) {
return i;
final int limit = getValidEndIndex();
if(negative){
for (int i = from; i > limit; i--) {
if (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) {
return i;
}
}
} else{
for (int i = from; i < limit; i++) {
if (NumberUtil.equals(c, text.charAt(i), caseInsensitive)) {
return i;
}
}
}
return -1;

View File

@ -4,7 +4,8 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Matcher;
/**
* 字符匹配查找器
* 字符匹配查找器<br>
* 查找满足指定{@link Matcher} 匹配的字符所在位置此类长用于查找某一类字符如数字等
*
* @since 5.7.14
* @author looly
@ -25,10 +26,18 @@ public class CharMatcherFinder extends TextFinder {
@Override
public int start(int from) {
Assert.notNull(this.text, "Text to find must be not null!");
final int length = text.length();
for (int i = from; i < length; i++) {
if(matcher.match(text.charAt(i))){
return i;
final int limit = getValidEndIndex();
if(negative){
for (int i = from; i > limit; i--) {
if(matcher.match(text.charAt(i))){
return i;
}
}
} else {
for (int i = from; i < limit; i++) {
if(matcher.match(text.charAt(i))){
return i;
}
}
}
return -1;

View File

@ -8,10 +8,12 @@ package cn.hutool.core.text.finder;
*/
public interface Finder {
int INDEX_NOT_FOUND = -1;
/**
* 返回开始位置即起始字符位置包含未找到返回-1
*
* @param from 查找的开始位置包含
* @param from 查找的开始位置包含
* @return 起始字符位置未找到返回-1
*/
int start(int from);

View File

@ -3,7 +3,8 @@ package cn.hutool.core.text.finder;
import cn.hutool.core.lang.Assert;
/**
* 固定长度查找器
* 固定长度查找器<br>
* 给定一个长度查找的位置为from + length一般用于分段截取
*
* @since 5.7.14
* @author looly
@ -24,9 +25,18 @@ public class LengthFinder extends TextFinder {
@Override
public int start(int from) {
Assert.notNull(this.text, "Text to find must be not null!");
final int result = from + length;
if(result < text.length()){
return result;
final int limit = getValidEndIndex();
int result;
if(negative){
result = from - length;
if(result > limit){
return result;
}
} else {
result = from + length;
if(result < limit){
return result;
}
}
return -1;
}

View File

@ -4,7 +4,8 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 正则查找器
* 正则查找器<br>
* 通过传入正则表达式查找指定字符串中匹配正则的开始和结束位置
*
* @author looly
* @since 5.7.14
@ -40,17 +41,32 @@ public class PatternFinder extends TextFinder {
return super.setText(text);
}
@Override
public TextFinder setNegative(boolean negative) {
throw new UnsupportedOperationException("Negative is invalid for Pattern!");
}
@Override
public int start(int from) {
if (matcher.find(from)) {
return matcher.start();
// 只有匹配到的字符串结尾在limit范围内才算找到
if(matcher.end() <= getValidEndIndex()){
return matcher.start();
}
}
return -1;
return INDEX_NOT_FOUND;
}
@Override
public int end(int start) {
return matcher.end();
final int end = matcher.end();
final int limit;
if(endIndex < 0){
limit = text.length();
}else{
limit = Math.min(endIndex, text.length());
}
return end < limit ? end : INDEX_NOT_FOUND;
}
@Override

View File

@ -1,10 +1,10 @@
package cn.hutool.core.text.finder;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.text.CharSequenceUtil;
/**
* 字符查找器
* 字符查找器
*
* @author looly
* @since 5.7.14
@ -12,25 +12,46 @@ import cn.hutool.core.util.StrUtil;
public class StrFinder extends TextFinder {
private static final long serialVersionUID = 1L;
private final CharSequence str;
private final CharSequence strToFind;
private final boolean caseInsensitive;
/**
* 构造
*
* @param str 被查找的字符串
* @param strToFind 被查找的字符串
* @param caseInsensitive 是否忽略大小写
*/
public StrFinder(CharSequence str, boolean caseInsensitive) {
Assert.notEmpty(str);
this.str = str;
public StrFinder(CharSequence strToFind, boolean caseInsensitive) {
Assert.notEmpty(strToFind);
this.strToFind = strToFind;
this.caseInsensitive = caseInsensitive;
}
@Override
public int start(int from) {
Assert.notNull(this.text, "Text to find must be not null!");
return StrUtil.indexOf(text, str, from, caseInsensitive);
final int subLen = strToFind.length();
if (from < 0) {
from = 0;
}
int endLimit = getValidEndIndex();
if (negative) {
for (int i = from; i > endLimit; i--) {
if (CharSequenceUtil.isSubEquals(text, i, strToFind, 0, subLen, caseInsensitive)) {
return i;
}
}
} else {
endLimit = endLimit - subLen + 1;
for (int i = from; i < endLimit; i++) {
if (CharSequenceUtil.isSubEquals(text, i, strToFind, 0, subLen, caseInsensitive)) {
return i;
}
}
}
return INDEX_NOT_FOUND;
}
@Override
@ -38,6 +59,6 @@ public class StrFinder extends TextFinder {
if (start < 0) {
return -1;
}
return start + str.length();
return start + strToFind.length();
}
}

View File

@ -1,5 +1,7 @@
package cn.hutool.core.text.finder;
import cn.hutool.core.lang.Assert;
import java.io.Serializable;
/**
@ -12,6 +14,8 @@ public abstract class TextFinder implements Finder, Serializable {
private static final long serialVersionUID = 1L;
protected CharSequence text;
protected int endIndex = -1;
protected boolean negative;
/**
* 设置被查找的文本
@ -20,7 +24,51 @@ public abstract class TextFinder implements Finder, Serializable {
* @return this
*/
public TextFinder setText(CharSequence text) {
this.text = text;
this.text = Assert.notNull(text, "Text must be not null!");
return this;
}
/**
* 设置查找的结束位置<br>
* 如果从前向后查找结束位置最大为text.length()<br>
* 如果从后向前结束位置为-1
*
* @param endIndex 结束位置不包括
* @return this
*/
public TextFinder setEndIndex(int endIndex) {
this.endIndex = endIndex;
return this;
}
/**
* 设置是否反向查找{@code true}表示从后向前查找
*
* @param negative 结束位置不包括
* @return this
*/
public TextFinder setNegative(boolean negative) {
this.negative = negative;
return this;
}
/**
* 获取有效结束位置<br>
* 如果{@link #endIndex}小于0在反向模式下是开头-1正向模式是结尾text.length()
*
* @return 有效结束位置
*/
protected int getValidEndIndex() {
if(negative && -1 == endIndex){
// 反向查找模式下-1表示0前面的位置即字符串反向末尾的位置
return -1;
}
final int limit;
if (endIndex < 0) {
limit = endIndex + text.length() + 1;
} else {
limit = Math.min(endIndex, text.length());
}
return limit;
}
}

View File

@ -0,0 +1,13 @@
/**
* 文本查找实现包括
* <ul>
* <li>查找文本中的字符正向反向</li>
* <li>查找文本中的匹配字符正向反向</li>
* <li>查找文本中的字符串正向反向</li>
* <li>查找文本中匹配正则的字符串正向</li>
* </ul>
*
* @author looly
*
*/
package cn.hutool.core.text.finder;

View File

@ -0,0 +1,60 @@
package cn.hutool.core.thread;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* {@link CompletableFuture}异步工具类<br>
* {@link CompletableFuture} Future 的改进可以通过传入回调对象在任务完成后调用之
*
* @author achao1441470436@gmail.com
* @since 5.7.17
*/
public class AsyncUtil {
/**
* 等待所有任务执行完毕包裹了异常
*
* @param tasks 并行任务
* @throws UndeclaredThrowableException 未受检异常
*/
public static void waitAll(CompletableFuture<?>... tasks) {
try {
CompletableFuture.allOf(tasks).get();
} catch (InterruptedException | ExecutionException e) {
throw new ThreadException(e);
}
}
/**
* 等待任意一个任务执行完毕包裹了异常
*
* @param tasks 并行任务
* @throws UndeclaredThrowableException 未受检异常
*/
public static void waitAny(CompletableFuture<?>... tasks) {
try {
CompletableFuture.anyOf(tasks).get();
} catch (InterruptedException | ExecutionException e) {
throw new ThreadException(e);
}
}
/**
* 获取异步任务结果包裹了异常
*
* @param task 异步任务
* @param <T> 任务返回值类型
* @return 任务返回值
* @throws RuntimeException 未受检异常
*/
public static <T> T get(CompletableFuture<T> task) {
try {
return task.get();
} catch (InterruptedException | ExecutionException e) {
throw new ThreadException(e);
}
}
}

View File

@ -0,0 +1,38 @@
package cn.hutool.core.thread;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
/**
* 工具类异常
*
* @author looly
* @since 5.7.17
*/
public class ThreadException extends RuntimeException {
private static final long serialVersionUID = 5253124428623713216L;
public ThreadException(Throwable e) {
super(ExceptionUtil.getMessage(e), e);
}
public ThreadException(String message) {
super(message);
}
public ThreadException(String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params));
}
public ThreadException(String message, Throwable throwable) {
super(message, throwable);
}
public ThreadException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
super(message, throwable, enableSuppression, writableStackTrace);
}
public ThreadException(Throwable throwable, String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params), throwable);
}
}

View File

@ -147,7 +147,8 @@ public class IdcardUtil {
}
/**
* 是否有效身份证号忽略X的大小写
* 是否有效身份证号忽略X的大小写<br>
* 如果身份证号码中含有空格始终返回{@code false}
*
* @param idCard 身份证号支持18位15位和港澳台的10位
* @return 是否有效
@ -157,7 +158,7 @@ public class IdcardUtil {
return false;
}
idCard = idCard.trim();
//idCard = idCard.trim();
int length = idCard.length();
switch (length) {
case 18:// 18位身份证

View File

@ -12,7 +12,6 @@ import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
/**
@ -1362,13 +1361,12 @@ public class NumberUtil {
throw new UtilException("Size is larger than range between begin and end!");
}
Random ran = new Random();
Set<Integer> set = new HashSet<>();
Set<Integer> set = new HashSet<>(size, 1);
while (set.size() < size) {
set.add(begin + ran.nextInt(end - begin));
set.add(begin + RandomUtil.randomInt(end - begin));
}
return set.toArray(new Integer[size]);
return set.toArray(new Integer[0]);
}
// ------------------------------------------------------------------------------------------- range

View File

@ -77,20 +77,14 @@ public class ReflectUtil {
* 获得一个类中所有构造列表
*
* @param <T> 构造的对象类型
* @param beanClass
* @param beanClass {@code null}
* @return 字段列表
* @throws SecurityException 安全检查异常
*/
@SuppressWarnings("unchecked")
public static <T> Constructor<T>[] getConstructors(Class<T> beanClass) throws SecurityException {
Assert.notNull(beanClass);
Constructor<?>[] constructors = CONSTRUCTORS_CACHE.get(beanClass);
if (null != constructors) {
return (Constructor<T>[]) constructors;
}
constructors = getConstructorsDirectly(beanClass);
return (Constructor<T>[]) CONSTRUCTORS_CACHE.put(beanClass, constructors);
return (Constructor<T>[]) CONSTRUCTORS_CACHE.get(beanClass, ()->getConstructorsDirectly(beanClass));
}
/**
@ -101,7 +95,6 @@ public class ReflectUtil {
* @throws SecurityException 安全检查异常
*/
public static Constructor<?>[] getConstructorsDirectly(Class<?> beanClass) throws SecurityException {
Assert.notNull(beanClass);
return beanClass.getDeclaredConstructors();
}
@ -179,13 +172,8 @@ public class ReflectUtil {
* @throws SecurityException 安全检查异常
*/
public static Field[] getFields(Class<?> beanClass) throws SecurityException {
Field[] allFields = FIELDS_CACHE.get(beanClass);
if (null != allFields) {
return allFields;
}
allFields = getFieldsDirectly(beanClass, true);
return FIELDS_CACHE.put(beanClass, allFields);
Assert.notNull(beanClass);
return FIELDS_CACHE.get(beanClass, ()->getFieldsDirectly(beanClass, true));
}
@ -641,18 +629,13 @@ public class ReflectUtil {
/**
* 获得一个类中所有方法列表包括其父类中的方法
*
* @param beanClass
* @param beanClass {@code null}
* @return 方法列表
* @throws SecurityException 安全检查异常
*/
public static Method[] getMethods(Class<?> beanClass) throws SecurityException {
Method[] allMethods = METHODS_CACHE.get(beanClass);
if (null != allMethods) {
return allMethods;
}
allMethods = getMethodsDirectly(beanClass, true);
return METHODS_CACHE.put(beanClass, allMethods);
Assert.notNull(beanClass);
return METHODS_CACHE.get(beanClass, ()-> getMethodsDirectly(beanClass, true));
}
/**

View File

@ -319,7 +319,8 @@ public class URLUtil extends URLEncodeUtil {
/**
* 解码application/x-www-form-urlencoded字符<br>
* %开头的16进制表示的内容解码
* %开头的16进制表示的内容解码<br>
* 规则见https://url.spec.whatwg.org/#urlencoded-parsing
*
* @param content 被解码内容
* @param charset 编码null表示不解码
@ -327,9 +328,6 @@ public class URLUtil extends URLEncodeUtil {
* @since 4.4.1
*/
public static String decode(String content, Charset charset) {
if (null == charset) {
return content;
}
return URLDecoder.decode(content, charset);
}
@ -344,9 +342,6 @@ public class URLUtil extends URLEncodeUtil {
* @since 5.6.3
*/
public static String decode(String content, Charset charset, boolean isPlusToSpace) {
if (null == charset) {
return content;
}
return URLDecoder.decode(content, charset, isPlusToSpace);
}
@ -360,7 +355,7 @@ public class URLUtil extends URLEncodeUtil {
* @throws UtilException UnsupportedEncodingException
*/
public static String decode(String content, String charset) throws UtilException {
return decode(content, CharsetUtil.charset(charset));
return decode(content, StrUtil.isEmpty(charset) ? null : CharsetUtil.charset(charset));
}
/**

View File

@ -0,0 +1,47 @@
package cn.hutool.core.clone;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DefaultCloneTest {
@Test
public void clone0() {
Car oldCar = new Car();
oldCar.setId(1);
oldCar.setWheelList(Stream.of(new Wheel("h")).collect(Collectors.toList()));
Car newCar = oldCar.clone0();
Assert.assertEquals(oldCar.getId(), newCar.getId());
Assert.assertEquals(oldCar.getWheelList(), newCar.getWheelList());
newCar.setId(2);
Assert.assertNotEquals(oldCar.getId(), newCar.getId());
newCar.getWheelList().add(new Wheel("s"));
Assert.assertNotSame(oldCar, newCar);
}
@Data
static class Car implements DefaultCloneable<Car> {
private Integer id;
private List<Wheel> wheelList;
}
@Data
@AllArgsConstructor
static class Wheel {
private String direction;
}
}

View File

@ -4,14 +4,14 @@ import org.junit.Assert;
import org.junit.Test;
public class ZodiacTest {
@Test
public void getZodiacTest() {
Assert.assertEquals("摩羯座", Zodiac.getZodiac(Month.JANUARY, 19));
Assert.assertEquals("水瓶座", Zodiac.getZodiac(Month.JANUARY, 20));
Assert.assertEquals("巨蟹座", Zodiac.getZodiac(6, 17));
}
@Test
public void getChineseZodiacTest() {
Assert.assertEquals("", Zodiac.getChineseZodiac(1994));

View File

@ -1,5 +1,6 @@
package cn.hutool.core.lang;
import cn.hutool.core.collection.CollectionUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -8,7 +9,11 @@ import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Stream;
/**
* {@link Opt}的单元测试
@ -43,11 +48,14 @@ public class OptTest {
@Test
@Ignore
public void ifPresentOrElseTest() {
// 这是jdk9中的新函数直接照搬了过来
// 存在就打印对应的值不存在则用{@code System.err.println}打印另一句字符串
Opt.ofNullable("Hello Hutool!").ifPresentOrElse(Console::log, () -> Console.error("Ops!Something is wrong!"));
Opt.empty().ifPresentOrElse(Console::log, () -> Console.error("Ops!Something is wrong!"));
// 拓展为支持链式调用
Opt.empty().ifPresentOrElse(Console::log, () -> Console.error("Ops!Something is wrong!"))
.ifPresentOrElse(Console::log, () -> Console.error("Ops!Something is wrong!"));
}
@Test
@ -126,6 +134,61 @@ public class OptTest {
Assert.assertNull(exceptionWithMessage);
}
@Test
public void flattedMapTest() {
// 和Optional兼容的flatMap
List<User> userList = new ArrayList<>();
// 以前不兼容
// Opt.ofNullable(userList).map(List::stream).flatMap(Stream::findFirst);
// 现在兼容
User user = Opt.ofNullable(userList).map(List::stream)
.flattedMap(Stream::findFirst).orElseGet(User.builder()::build);
Assert.assertNull(user.getUsername());
Assert.assertNull(user.getNickname());
}
@Test
public void ofEmptyAbleTest() {
// 以前输入一个CollectionUtil感觉要命类似前缀的类一大堆代码补全形同虚设(在项目中起码要输入完CollectionUtil才能在第一个调出这个函数)
// 关键它还很常用判空和判空集合真的太常用了...
List<String> past = Opt.ofNullable(Collections.<String>emptyList()).filter(CollectionUtil::isNotEmpty).orElseGet(() -> Collections.singletonList("hutool"));
// 现在一个ofEmptyAble搞定
List<String> hutool = Opt.ofEmptyAble(Collections.<String>emptyList()).orElseGet(() -> Collections.singletonList("hutool"));
Assert.assertEquals(past, hutool);
Assert.assertEquals(hutool, Collections.singletonList("hutool"));
}
@Test
public void mapOrElseTest() {
// 如果值存在就转换为大写否则打印一句字符串支持链式调用转换为其他类型
String hutool = Opt.ofBlankAble("hutool").mapOrElse(String::toUpperCase, () -> Console.log("yes")).mapOrElse(String::intern, () -> Console.log("Value is not present~")).get();
Assert.assertEquals("HUTOOL", hutool);
}
@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection", "ConstantConditions"})
@Test
public void execTest() {
// 有一些资深的程序员跟我说你这个lambda双冒号语法糖看不懂...
// 为了尊重资深程序员的意见并且提升代码可读性封装了一下 "try catch NPE 和 数组越界"的情况
// 以前这种写法简洁但可读性稍低对资深程序员不太友好
List<String> last = null;
String npeSituation = Opt.ofEmptyAble(last).flattedMap(l -> l.stream().findFirst()).orElse("hutool");
String indexOutSituation = Opt.ofEmptyAble(last).map(l -> l.get(0)).orElse("hutool");
// 现在代码整洁度降低但可读性up如果再人说看不懂这代码...
String npe = Opt.exec(() -> last.get(0)).orElse("hutool");
String indexOut = Opt.exec(() -> {
List<String> list = new ArrayList<>();
// 你可以在里面写一长串调用链 list.get(0).getUser().getId()
return list.get(0);
}).orElse("hutool");
Assert.assertEquals(npe, npeSituation);
Assert.assertEquals(indexOut, indexOutSituation);
Assert.assertEquals("hutool", npe);
Assert.assertEquals("hutool", indexOut);
}
@Data
@Builder
@NoArgsConstructor

View File

@ -76,4 +76,48 @@ public class TreeTest {
Assert .assertEquals(7, ids.size());
}
@Test
public void cloneTreeTest(){
final Tree<String> tree = TreeUtil.buildSingle(nodeList, "0");
final Tree<String> cloneTree = tree.cloneTree();
List<String> ids = new ArrayList<>();
cloneTree.walk((tr)-> ids.add(tr.getId()));
Assert .assertEquals(7, ids.size());
}
@Test
public void filterTest(){
// 经过过滤丢掉"用户添加"节点
final Tree<String> tree = TreeUtil.buildSingle(nodeList, "0");
tree.filter((t)->{
final CharSequence name = t.getName();
return null != name && name.toString().contains("管理");
});
List<String> ids = new ArrayList<>();
tree.walk((tr)-> ids.add(tr.getId()));
Assert .assertEquals(6, ids.size());
}
@Test
public void filterNewTest(){
final Tree<String> tree = TreeUtil.buildSingle(nodeList, "0");
// 经过过滤生成新的树
Tree<String> newTree = tree.filterNew((t)->{
final CharSequence name = t.getName();
return null != name && name.toString().contains("管理");
});
List<String> ids = new ArrayList<>();
newTree.walk((tr)-> ids.add(tr.getId()));
Assert .assertEquals(6, ids.size());
List<String> ids2 = new ArrayList<>();
tree.walk((tr)-> ids2.add(tr.getId()));
Assert .assertEquals(7, ids2.size());
}
}

View File

@ -0,0 +1,17 @@
package cn.hutool.core.net;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Test;
public class FormUrlencodedTest {
@Test
public void encodeParamTest(){
String encode = FormUrlencoded.ALL.encode("a+b", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a%2Bb", encode);
encode = FormUrlencoded.ALL.encode("a b", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a+b", encode);
}
}

View File

@ -0,0 +1,14 @@
package cn.hutool.core.net;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Test;
public class RFC3986Test {
@Test
public void encodeQueryTest(){
final String encode = RFC3986.QUERY_PARAM_VALUE.encode("a=b", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a=b", encode);
}
}

View File

@ -1,17 +0,0 @@
package cn.hutool.core.net;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Test;
public class URLEncoderTest {
@Test
public void encodeTest(){
String encode = URLEncoder.DEFAULT.encode("+", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("+", encode);
encode = URLEncoder.DEFAULT.encode(" ", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("%20", encode);
}
}

View File

@ -3,6 +3,7 @@ package cn.hutool.core.net;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.URLUtil;
import org.junit.Assert;
import org.junit.Test;
@ -99,4 +100,26 @@ public class UrlQueryTest {
query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);
Assert.assertEquals("password==&username%3D=SSM", query);
}
@Test
public void plusTest(){
// 根据RFC3986在URL中+是安全字符即此符号不转义
final String a = UrlQuery.of(MapUtil.of("a+b", "1+2")).build(CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a+b=1+2", a);
}
@Test
public void parsePlusTest(){
// 根据RFC3986在URL中+是安全字符即此符号不转义
final String a = UrlQuery.of("a+b=1+2", CharsetUtil.CHARSET_UTF_8)
.build(CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a+b=1+2", a);
}
@Test
public void spaceTest(){
// 根据RFC3986在URL中空格编码为"%20"
final String a = UrlQuery.of(MapUtil.of("a ", " ")).build(CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a%20=%20", a);
}
}

View File

@ -47,5 +47,22 @@ public class CharSequenceUtilTest {
Assert.assertEquals(str1, str2);
}
// ------------------------------------------------------------------------ remove
@Test
public void indexOfTest(){
int index = CharSequenceUtil.indexOf("abc123", '1');
Assert.assertEquals(3, index);
index = CharSequenceUtil.indexOf("abc123", '3');
Assert.assertEquals(5, index);
index = CharSequenceUtil.indexOf("abc123", 'a');
Assert.assertEquals(0, index);
}
@Test
public void indexOfTest2(){
int index = CharSequenceUtil.indexOf("abc123", '1', 0, 3);
Assert.assertEquals(-1, index);
index = CharSequenceUtil.indexOf("abc123", 'b', 0, 3);
Assert.assertEquals(1, index);
}
}

View File

@ -0,0 +1,30 @@
package cn.hutool.core.text.finder;
import org.junit.Assert;
import org.junit.Test;
public class CharFinderTest {
@Test
public void startTest(){
int start = new CharFinder('a').setText("cba123").start(2);
Assert.assertEquals(2, start);
start = new CharFinder('c').setText("cba123").start(2);
Assert.assertEquals(-1, start);
start = new CharFinder('3').setText("cba123").start(2);
Assert.assertEquals(5, start);
}
@Test
public void negativeStartTest(){
int start = new CharFinder('a').setText("cba123").setNegative(true).start(2);
Assert.assertEquals(2, start);
start = new CharFinder('2').setText("cba123").setNegative(true).start(2);
Assert.assertEquals(-1, start);
start = new CharFinder('c').setText("cba123").setNegative(true).start(2);
Assert.assertEquals(0, start);
}
}

View File

@ -135,4 +135,18 @@ public class SplitIterTest {
final List<String> strings = splitIter.toList(false);
Assert.assertEquals(1, strings.size());
}
// 切割字符串是空字符串时报错
@Test(expected = IllegalArgumentException.class)
public void splitByEmptyTest(){
String text = "aa,bb,cc";
SplitIter splitIter = new SplitIter(text,
new StrFinder("", false),
3,
false
);
final List<String> strings = splitIter.toList(false);
Assert.assertEquals(1, strings.size());
}
}

View File

@ -0,0 +1,38 @@
package cn.hutool.core.thread;
import cn.hutool.core.lang.Assert;
import org.junit.Ignore;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* CompletableFuture工具类测试
*
* @author <achao1441470436@gmail.com>
* @since 2021/11/10 0010 21:15
*/
public class AsyncUtilTest {
@Test
@Ignore
public void waitAndGetTest() {
CompletableFuture<String> hutool = CompletableFuture.supplyAsync(() -> {
ThreadUtil.sleep(1, TimeUnit.SECONDS);
return "hutool";
});
CompletableFuture<String> sweater = CompletableFuture.supplyAsync(() -> {
ThreadUtil.sleep(2, TimeUnit.SECONDS);
return "卫衣";
});
CompletableFuture<String> warm = CompletableFuture.supplyAsync(() -> {
ThreadUtil.sleep(3, TimeUnit.SECONDS);
return "真暖和";
});
// 等待完成
AsyncUtil.waitAll(hutool, sweater, warm);
// 获取结果
Assert.isTrue("hutool卫衣真暖和".equals(AsyncUtil.get(hutool) + AsyncUtil.get(sweater) + AsyncUtil.get(warm)));
}
}

View File

@ -400,4 +400,10 @@ public class NumberUtilTest {
final String s = new BigDecimal(num).toPlainString();
Assert.assertEquals("5344342.34", s);
}
@Test
public void generateBySetTest(){
final Integer[] integers = NumberUtil.generateBySet(10, 100, 5);
Assert.assertEquals(5, integers.length);
}
}

View File

@ -177,7 +177,7 @@ public class StrUtilTest {
Assert.assertEquals(5, StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3));
Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9));
Assert.assertEquals(2, StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1));
Assert.assertEquals(2, StrUtil.indexOfIgnoreCase("aabaabaa", "", 2));
Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase("aabaabaa", "", 2));
Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase("abc", "", 9));
}
@ -199,8 +199,8 @@ public class StrUtilTest {
Assert.assertEquals(2, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B", 3));
Assert.assertEquals(5, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B", 9));
Assert.assertEquals(-1, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B", -1));
Assert.assertEquals(2, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "", 2));
Assert.assertEquals(3, StrUtil.lastIndexOfIgnoreCase("abc", "", 9));
Assert.assertEquals(-1, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "", 2));
Assert.assertEquals(-1, StrUtil.lastIndexOfIgnoreCase("abc", "", 9));
Assert.assertEquals(0, StrUtil.lastIndexOfIgnoreCase("AAAcsd", "aaa"));
}
@ -384,6 +384,12 @@ public class StrUtilTest {
String abc1d = StrUtil.toCamelCase("abc_1d");
Assert.assertEquals("abc1d", abc1d);
String str2 = "Table-Test-Of-day";
String result2 = StrUtil.toCamelCase(str2, CharUtil.DASHED);
System.out.println(result2);
Assert.assertEquals("tableTestOfDay", result2);
}
@Test

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-cron</artifactId>

View File

@ -110,9 +110,10 @@ public class CronUtil {
* 移除任务
*
* @param schedulerId 任务ID
* @return 是否移除成功{@code false}表示未找到对应ID的任务
*/
public static void remove(String schedulerId) {
scheduler.deschedule(schedulerId);
public static boolean remove(String schedulerId) {
return scheduler.descheduleWithStatus(schedulerId);
}
/**

View File

@ -289,10 +289,21 @@ public class Scheduler implements Serializable {
* @return this
*/
public Scheduler deschedule(String id) {
this.taskTable.remove(id);
descheduleWithStatus(id);
return this;
}
/**
* 移除Task并返回是否移除成功
*
* @param id Task的ID
* @return 是否移除成功{@code false}表示未找到对应ID的任务
* @since 5.7.17
*/
public boolean descheduleWithStatus(String id) {
return this.taskTable.remove(id);
}
/**
* 更新Task执行的时间规则
*

View File

@ -1,5 +1,6 @@
package cn.hutool.cron;
import cn.hutool.core.util.StrUtil;
import cn.hutool.cron.pattern.CronPattern;
import cn.hutool.cron.task.CronTask;
import cn.hutool.cron.task.Task;
@ -128,21 +129,24 @@ public class TaskTable implements Serializable {
* 移除Task
*
* @param id Task的ID
* @return 是否成功移除{@code false}表示未找到对应ID的任务
*/
public void remove(String id) {
public boolean remove(String id) {
final Lock writeLock = lock.writeLock();
writeLock.lock();
try {
final int index = ids.indexOf(id);
if (index > -1) {
tasks.remove(index);
patterns.remove(index);
ids.remove(index);
size--;
if (index < 0) {
return false;
}
tasks.remove(index);
patterns.remove(index);
ids.remove(index);
size--;
} finally {
writeLock.unlock();
}
return true;
}
/**
@ -268,6 +272,16 @@ public class TaskTable implements Serializable {
}
}
@Override
public String toString() {
final StringBuilder builder = StrUtil.builder();
for (int i = 0; i < size; i++) {
builder.append(StrUtil.format("[{}] [{}] [{}]\n",
ids.get(i), patterns.get(i), tasks.get(i)));
}
return builder.toString();
}
/**
* 如果时间匹配则执行相应的Task无锁
*
@ -282,4 +296,4 @@ public class TaskTable implements Serializable {
}
}
}
}
}

View File

@ -68,7 +68,6 @@ public class TimingWheel {
* @param consumer 任务处理器
*/
public TimingWheel(long tickMs, int wheelSize, long currentTime, Consumer<TimerTaskList> consumer) {
this.currentTime = currentTime;
this.tickMs = tickMs;
this.wheelSize = wheelSize;
this.interval = tickMs * wheelSize;

View File

@ -0,0 +1,21 @@
package cn.hutool.cron;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.IdUtil;
import cn.hutool.cron.pattern.CronPattern;
import org.junit.Ignore;
import org.junit.Test;
public class TaskTableTest {
@Test
@Ignore
public void toStringTest(){
final TaskTable taskTable = new TaskTable();
taskTable.add(IdUtil.fastUUID(), new CronPattern("*/10 * * * * *"), ()-> Console.log("Task 1"));
taskTable.add(IdUtil.fastUUID(), new CronPattern("*/20 * * * * *"), ()-> Console.log("Task 2"));
taskTable.add(IdUtil.fastUUID(), new CronPattern("*/30 * * * * *"), ()-> Console.log("Task 3"));
Console.log(taskTable);
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-crypto</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-db</artifactId>
@ -81,7 +81,7 @@
<dependency>
<groupId>com.github.chris2018998</groupId>
<artifactId>beecp</artifactId>
<version>3.2.7</version>
<version>3.2.9</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
@ -149,7 +149,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.0</version>
<version>42.3.1</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-dfa</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-extra</artifactId>
@ -19,7 +19,7 @@
<properties>
<!-- versions -->
<velocity.version>2.3</velocity.version>
<beetl.version>3.7.0.RELEASE</beetl.version>
<beetl.version>3.8.1.RELEASE</beetl.version>
<rythm.version>1.4.1</rythm.version>
<freemarker.version>2.3.31</freemarker.version>
<enjoy.version>4.9.16</enjoy.version>
@ -384,7 +384,7 @@
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.5</version>
<version>1.2.6</version>
<scope>test</scope>
<exclusions>
<exclusion>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-http</artifactId>

View File

@ -39,7 +39,11 @@ public enum ContentType {
/**
* text/html编码
*/
TEXT_HTML("text/html");
TEXT_HTML("text/html"),
/**
* application/octet-stream编码
*/
OCTET_STREAM("application/octet-stream");
private final String value;

View File

@ -3,7 +3,6 @@ package cn.hutool.http;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.BytesResource;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.io.resource.MultiFileResource;
@ -15,14 +14,16 @@ import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.body.BytesBody;
import cn.hutool.http.body.FormUrlEncodedBody;
import cn.hutool.http.body.MultipartBody;
import cn.hutool.http.body.RequestBody;
import cn.hutool.http.cookie.GlobalCookieManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
@ -1211,23 +1212,13 @@ public class HttpRequest extends HttpBase<HttpRequest> {
}
// Write的时候会优先使用body中的内容write时自动关闭OutputStream
byte[] content;
RequestBody body;
if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
content = this.bodyBytes;
body = BytesBody.create(this.bodyBytes);
} else {
content = StrUtil.bytes(getFormUrlEncoded(), this.charset);
body = FormUrlEncodedBody.create(this.form, this.charset);
}
IoUtil.write(this.httpConnection.getOutputStream(), true, content);
}
/**
* 获取编码后的表单数据无表单数据返回""
*
* @return 编码后的表单数据无表单数据返回""
* @since 5.3.2
*/
private String getFormUrlEncoded() {
return HttpUtil.toParams(this.form, this.charset);
body.writeClose(this.httpConnection.getOutputStream());
}
/**
@ -1237,18 +1228,9 @@ public class HttpRequest extends HttpBase<HttpRequest> {
* @throws IOException IO异常
*/
private void sendMultipart() throws IOException {
setMultipart();// 设置表单类型为Multipart
try (OutputStream out = this.httpConnection.getOutputStream()) {
MultipartBody.create(this.form, this.charset).write(out);
}
}
/**
* 设置表单类型为Multipart文件上传
*/
private void setMultipart() {
//设置表单类型为Multipart文件上传
this.httpConnection.header(Header.CONTENT_TYPE, MultipartBody.getContentType(), true);
MultipartBody.create(this.form, this.charset).writeClose(this.httpConnection.getOutputStream());
}
/**

View File

@ -0,0 +1,56 @@
package cn.hutool.http;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
/**
* HTTP资源可自定义Content-Type
*
* @author looly
* @since 5.7.17
*/
public class HttpResource implements Resource, Serializable {
private static final long serialVersionUID = 1L;
private final Resource resource;
private final String contentType;
/**
* 构造
*
* @param resource 资源非空
* @param contentType Content-Type类型{@code null}表示不设置
*/
public HttpResource(Resource resource, String contentType) {
this.resource = Assert.notNull(resource, "Resource must be not null !");
this.contentType = contentType;
}
@Override
public String getName() {
return resource.getName();
}
@Override
public URL getUrl() {
return resource.getUrl();
}
@Override
public InputStream getStream() {
return resource.getStream();
}
/**
* 获取自定义Content-Type类型
*
* @return Content-Type类型
*/
public String getContentType() {
return this.contentType;
}
}

View File

@ -6,6 +6,7 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.RFC3986;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.CharsetUtil;
@ -452,7 +453,8 @@ public class HttpUtil {
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值
* 会自动url编码键和值<br>
* 此方法用于拼接URL中的Query部分并不适用于POST请求中的表单
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
@ -461,9 +463,10 @@ public class HttpUtil {
* @param paramMap 表单数据
* @param charset 编码{@code null} 表示不encode键值对
* @return url参数
* @see #toParams(Map, Charset, boolean)
*/
public static String toParams(Map<String, ?> paramMap, Charset charset) {
return UrlQuery.of(paramMap).build(charset);
return toParams(paramMap, charset, false);
}
/**
@ -477,14 +480,12 @@ public class HttpUtil {
*
* @param paramMap 表单数据
* @param charset 编码null表示不encode键值对
* @param isEncode 是否转义键和值
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @return url参数
* @since 5.7.13
* @deprecated 请使用 {@link #toParams(Map, Charset)}, charset为null表示不编码
* @since 5.7.16
*/
@Deprecated
public static String toParams(Map<String, ?> paramMap, Charset charset, boolean isEncode) {
return toParams(paramMap, isEncode ? charset : null);
public static String toParams(Map<String, ?> paramMap, Charset charset, boolean isFormUrlEncoded) {
return UrlQuery.of(paramMap, isFormUrlEncoded).build(charset);
}
/**
@ -557,9 +558,10 @@ public class HttpUtil {
if (null == name) {
// 对于像&a&这类无参数值的字符串我们将name为a的值设为""
name = paramPart.substring(pos, i);
builder.append(URLUtil.encodeQuery(name, charset)).append('=');
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=');
} else {
builder.append(URLUtil.encodeQuery(name, charset)).append('=').append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset)).append('&');
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=')
.append(RFC3986.QUERY_PARAM_VALUE.encode(paramPart.substring(pos, i), charset)).append('&');
}
name = null;
}

View File

@ -0,0 +1,172 @@
package cn.hutool.http;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.io.resource.StringResource;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.body.MultipartBody;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
/**
* Multipart/form-data输出流封装<br>
* 遵循RFC2388规范
*
* @since 5.7.17
* @author looly
*/
public class MultipartOutputStream extends OutputStream {
private static final String BOUNDARY = MultipartBody.BOUNDARY;
private static final String BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY);
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n";
private final OutputStream out;
private final Charset charset;
private boolean isFinish;
/**
* 构造
*
* @param out HTTP写出流
* @param charset 编码
*/
public MultipartOutputStream(OutputStream out, Charset charset) {
this.out = out;
this.charset = charset;
}
/**
* 添加Multipart表单的数据项<br>
* <pre>
* --分隔符(boundary)[换行]
* Content-Disposition: form-data; name="参数名"[换行]
* [换行]
* 参数值[换行]
* </pre>
* <p>
* 或者
*
* <pre>
* --分隔符(boundary)[换行]
* Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
* Content-Type: MIME类型[换行]
* [换行]
* 文件的二进制内容[换行]
* </pre>
*
* @param formFieldName 表单名
* @param value 可以是普通值资源如文件等
* @throws IORuntimeException IO异常
*/
public MultipartOutputStream write(String formFieldName, Object value) throws IORuntimeException {
// 多资源
if (value instanceof MultiResource) {
for (Resource subResource : (MultiResource) value) {
write(formFieldName, subResource);
}
return this;
}
// --分隔符(boundary)[换行]
beginPart();
if (value instanceof Resource) {
appendResource(formFieldName, (Resource) value);
} else {
appendResource(formFieldName,
new StringResource(Convert.toStr(value), null, this.charset));
}
write(StrUtil.CRLF);
return this;
}
@Override
public void write(int b) throws IOException {
this.out.write(b);
}
/**
* 上传表单结束
*
* @throws IORuntimeException IO异常
*/
public void finish() throws IORuntimeException {
if(false == isFinish){
write(BOUNDARY_END);
this.isFinish = true;
}
}
@Override
public void close() {
finish();
IoUtil.close(this.out);
}
/**
* 添加Multipart表单的Resource数据项支持包括{@link HttpResource}资源格式
*
* @param formFieldName 表单名
* @param resource 资源
* @throws IORuntimeException IO异常
*/
private void appendResource(String formFieldName, Resource resource) throws IORuntimeException {
final String fileName = resource.getName();
// Content-Disposition
if (null == fileName) {
// Content-Disposition: form-data; name="参数名"[换行]
write(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName));
} else {
// Content-Disposition: form-data; name="参数名"; filename="文件名"[换行]
write(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, fileName));
}
// Content-Type
if (resource instanceof HttpResource) {
final String contentType = ((HttpResource) resource).getContentType();
if (StrUtil.isNotBlank(contentType)) {
// Content-Type: 类型[换行]
write(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, contentType));
}
} else if(StrUtil.isNotEmpty(fileName)){
// 根据name的扩展名指定互联网媒体类型默认二进制流数据
write(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE,
HttpUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue())));
}
// 内容
write("\r\n");
resource.writeTo(this);
}
/**
* part开始写出:<br>
* <pre>
* --分隔符(boundary)[换行]
* </pre>
*/
private void beginPart(){
// --分隔符(boundary)[换行]
write("--", BOUNDARY, StrUtil.CRLF);
}
/**
* 写出对象
*
* @param objs 写出的对象转换为字符串
*/
private void write(Object... objs) {
IoUtil.write(this, this.charset, false, objs);
}
}

View File

@ -0,0 +1,39 @@
package cn.hutool.http.body;
import cn.hutool.core.io.IoUtil;
import java.io.OutputStream;
/**
* bytes类型的Http request body主要发送编码后的表单数据或rest body如JSON或XML
*
* @since 5.7.17
* @author looly
*/
public class BytesBody implements RequestBody {
private final byte[] content;
/**
* 创建 Http request body
* @param content body内容编码后
* @return BytesBody
*/
public static BytesBody create(byte[] content){
return new BytesBody(content);
}
/**
* 构造
*
* @param content Body内容编码后
*/
public BytesBody(byte[] content) {
this.content = content;
}
@Override
public void write(OutputStream out) {
IoUtil.write(out, false, content);
}
}

View File

@ -0,0 +1,38 @@
package cn.hutool.http.body;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.util.StrUtil;
import java.nio.charset.Charset;
import java.util.Map;
/**
* application/x-www-form-urlencoded 类型请求body封装
*
* @author looly
* @since 5.7.17
*/
public class FormUrlEncodedBody extends BytesBody {
/**
* 创建 Http request body
*
* @param form 表单
* @param charset 编码
* @return FormUrlEncodedBody
*/
public static FormUrlEncodedBody create(Map<String, Object> form, Charset charset) {
return new FormUrlEncodedBody(form, charset);
}
/**
* 构造
*
* @param form 表单
* @param charset 编码
*/
public FormUrlEncodedBody(Map<String, Object> form, Charset charset) {
super(StrUtil.bytes(UrlQuery.of(form, true).build(charset), charset));
}
}

View File

@ -1,35 +1,27 @@
package cn.hutool.http.body;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.MultipartOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Map;
/**
* Multipart/form-data数据的请求体封装
* Multipart/form-data数据的请求体封装<br>
* 遵循RFC2388规范
*
* @author looly
* @since 5.3.5
*/
public class MultipartBody implements RequestBody{
private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
private static final String BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY);
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
public class MultipartBody implements RequestBody {
public static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary=";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
/**
* 存储表单数据
@ -42,11 +34,12 @@ public class MultipartBody implements RequestBody{
/**
* 根据已有表单内容构建MultipartBody
* @param form 表单
*
* @param form 表单
* @param charset 编码
* @return MultipartBody
*/
public static MultipartBody create(Map<String, Object> form, Charset charset){
public static MultipartBody create(Map<String, Object> form, Charset charset) {
return new MultipartBody(form, charset);
}
@ -55,15 +48,15 @@ public class MultipartBody implements RequestBody{
*
* @return Multipart的Content-Type类型
*/
public static String getContentType(){
public static String getContentType() {
return CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY;
}
/**
* 构造
*
* @param form 表单
* @param charset 编码
* @param form 表单
* @param charset 编码
*/
public MultipartBody(Map<String, Object> form, Charset charset) {
this.form = form;
@ -77,78 +70,17 @@ public class MultipartBody implements RequestBody{
*/
@Override
public void write(OutputStream out) {
writeForm(out);
formEnd(out);
}
// 普通字符串数据
/**
* 发送文件对象表单
*
* @param out 输出流
*/
private void writeForm(OutputStream out) {
final MultipartOutputStream stream = new MultipartOutputStream(out, this.charset);
if (MapUtil.isNotEmpty(this.form)) {
for (Map.Entry<String, Object> entry : this.form.entrySet()) {
appendPart(entry.getKey(), entry.getValue(), out);
}
this.form.forEach(stream::write);
}
stream.finish();
}
/**
* 添加Multipart表单的数据项
*
* @param formFieldName 表单名
* @param value 可以是普通值资源如文件等
* @param out Http流
* @throws IORuntimeException IO异常
*/
private void appendPart(String formFieldName, Object value, OutputStream out) throws IORuntimeException {
// 多资源
if (value instanceof MultiResource) {
for (Resource subResource : (MultiResource) value) {
appendPart(formFieldName, subResource, out);
}
return;
}
write(out, "--", BOUNDARY, StrUtil.CRLF);
if(value instanceof Resource){
// 文件资源二进制资源
final Resource resource = (Resource)value;
final String fileName = resource.getName();
write(out, StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName)));
// 根据name的扩展名指定互联网媒体类型默认二进制流数据
write(out, StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
resource.writeTo(out);
} else{
// 普通数据
write(out, StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName));
write(out, value);
}
write(out, StrUtil.CRLF);
}
/**
* 上传表单结束
*
* @param out 输出流
* @throws IORuntimeException IO异常
*/
private void formEnd(OutputStream out) throws IORuntimeException {
write(out, BOUNDARY_END);
}
/**
* 写出对象
*
* @param out 输出流
* @param objs 写出的对象转换为字符串
*/
private void write(OutputStream out, Object... objs) {
IoUtil.write(out, this.charset, false, objs);
@Override
public String toString() {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
write(out);
return IoUtil.toStr(out, this.charset);
}
}

View File

@ -1,5 +1,7 @@
package cn.hutool.http.body;
import cn.hutool.core.io.IoUtil;
import java.io.OutputStream;
/**
@ -13,4 +15,18 @@ public interface RequestBody {
* @param out out流
*/
void write(OutputStream out);
/**
* 写出并关闭{@link OutputStream}
*
* @param out {@link OutputStream}
* @since 5.7.17
*/
default void writeClose(OutputStream out) {
try {
write(out);
} finally {
IoUtil.close(out);
}
}
}

View File

@ -148,4 +148,5 @@ public class HttpRequestTest {
HttpResponse execute = get.execute();
Console.log(execute.body());
}
}

View File

@ -0,0 +1,28 @@
package cn.hutool.http.body;
import cn.hutool.core.io.resource.StringResource;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpResource;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class MultipartBodyTest {
@Test
public void buildTest(){
Map<String, Object> form = new HashMap<>();
form.put("pic1", "pic1 content");
form.put("pic2", new HttpResource(
new StringResource("pic2 content"), "text/plain"));
form.put("pic3", new HttpResource(
new StringResource("pic3 content", "pic3.jpg"), "image/jpeg"));
final MultipartBody body = MultipartBody.create(form, CharsetUtil.CHARSET_UTF_8);
Assert.assertNotNull(body.toString());
// Console.log(body);
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-json</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-jwt</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-log</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-poi</artifactId>

View File

@ -6,6 +6,7 @@ import cn.hutool.poi.excel.cell.CellEditor;
import cn.hutool.poi.excel.cell.CellHandler;
import cn.hutool.poi.excel.cell.CellUtil;
import cn.hutool.poi.excel.reader.BeanSheetReader;
import cn.hutool.poi.excel.reader.ColumnSheetReader;
import cn.hutool.poi.excel.reader.ListSheetReader;
import cn.hutool.poi.excel.reader.MapSheetReader;
import cn.hutool.poi.excel.reader.SheetReader;
@ -78,8 +79,8 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
/**
* 构造
*
* @param bookStream Excel文件的流
* @param sheetIndex sheet序号0表示第一个sheet
* @param bookStream Excel文件的流
* @param sheetIndex sheet序号0表示第一个sheet
*/
public ExcelReader(InputStream bookStream, int sheetIndex) {
this(WorkbookUtil.createBook(bookStream), sheetIndex);
@ -88,8 +89,8 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
/**
* 构造
*
* @param bookStream Excel文件的流
* @param sheetName sheet名第一个默认是sheet1
* @param bookStream Excel文件的流
* @param sheetName sheet名第一个默认是sheet1
*/
public ExcelReader(InputStream bookStream, String sheetName) {
this(WorkbookUtil.createBook(bookStream), sheetName);
@ -237,8 +238,8 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
/**
* 读取工作簿中指定的Sheet
*
* @param startRowIndex 起始行包含从0开始计数
* @param endRowIndex 结束行包含从0开始计数
* @param startRowIndex 起始行包含从0开始计数
* @param endRowIndex 结束行包含从0开始计数
* @param aliasFirstLine 是否首行作为标题行转换别名
* @return 行的集合一行使用List表示
* @since 5.4.4
@ -251,11 +252,40 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
return read(reader);
}
/**
* 读取工作簿中指定的Sheet中指定列
*
* @param columnIndex 列号从0开始计数
* @param startRowIndex 起始行包含从0开始计数
* @return 列的集合
* @since 5.7.17
*/
public List<Object> readColumn(int columnIndex, int startRowIndex) {
return readColumn(columnIndex, startRowIndex, Integer.MAX_VALUE);
}
/**
* 读取工作簿中指定的Sheet中指定列
*
* @param columnIndex 列号从0开始计数
* @param startRowIndex 起始行包含从0开始计数
* @param endRowIndex 结束行包含从0开始计数
* @return 列的集合
* @since 5.7.17
*/
public List<Object> readColumn(int columnIndex, int startRowIndex, int endRowIndex) {
final ColumnSheetReader reader = new ColumnSheetReader(columnIndex, startRowIndex, endRowIndex);
reader.setCellEditor(this.cellEditor);
reader.setIgnoreEmptyRow(this.ignoreEmptyRow);
reader.setHeaderAlias(headerAlias);
return read(reader);
}
/**
* 读取工作簿中指定的Sheet此方法为类流处理方式当读到指定单元格时会调用CellEditor接口<br>
* 用户通过实现此接口可以更加灵活的处理每个单元格的数据
*
* @param cellHandler 单元格处理器用于处理读到的单元格及其数据
* @param cellHandler 单元格处理器用于处理读到的单元格及其数据
* @since 5.3.8
*/
public void read(CellHandler cellHandler) {
@ -268,7 +298,7 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
*
* @param startRowIndex 起始行包含从0开始计数
* @param endRowIndex 结束行包含从0开始计数
* @param cellHandler 单元格处理器用于处理读到的单元格及其数据
* @param cellHandler 单元格处理器用于处理读到的单元格及其数据
* @since 5.3.8
*/
public void read(int startRowIndex, int endRowIndex, CellHandler cellHandler) {
@ -281,7 +311,7 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
short columnSize;
for (int y = startRowIndex; y <= endRowIndex; y++) {
row = this.sheet.getRow(y);
if(null != row){
if (null != row) {
columnSize = row.getLastCellNum();
Cell cell;
for (short x = 0; x < columnSize; x++) {
@ -365,12 +395,12 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
/**
* 读取数据为指定类型
*
* @param <T> 读取数据类型
* @param <T> 读取数据类型
* @param sheetReader {@link SheetReader}实现
* @return 数据读取结果
* @since 5.4.4
*/
public <T> T read(SheetReader<T> sheetReader){
public <T> T read(SheetReader<T> sheetReader) {
checkNotClosed();
return Assert.notNull(sheetReader).read(this.sheet);
}

View File

@ -0,0 +1,48 @@
package cn.hutool.poi.excel.reader;
import cn.hutool.poi.excel.cell.CellUtil;
import org.apache.poi.ss.usermodel.Sheet;
import java.util.ArrayList;
import java.util.List;
/**
* 读取单独一列
*
* @author looly
* @since 5.7.17
*/
public class ColumnSheetReader extends AbstractSheetReader<List<Object>> {
private final int columnIndex;
/**
* 构造
*
* @param columnIndex 列号从0开始计数
* @param startRowIndex 起始行包含从0开始计数
* @param endRowIndex 结束行包含从0开始计数
*/
public ColumnSheetReader(int columnIndex, int startRowIndex, int endRowIndex) {
super(startRowIndex, endRowIndex);
this.columnIndex = columnIndex;
}
@Override
public List<Object> read(Sheet sheet) {
final List<Object> resultList = new ArrayList<>();
int startRowIndex = Math.max(this.startRowIndex, sheet.getFirstRowNum());// 读取起始行包含
int endRowIndex = Math.min(this.endRowIndex, sheet.getLastRowNum());// 读取结束行包含
Object value;
for (int i = startRowIndex; i <= endRowIndex; i++) {
value = CellUtil.getCellValue(CellUtil.getCell(sheet.getRow(i), columnIndex), cellEditor);
if(null != value || false == ignoreEmptyRow){
resultList.add(value);
}
}
return resultList;
}
}

View File

@ -40,12 +40,12 @@ public abstract class AbstractRowHandler<T> implements RowHandler {
}
@Override
public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
public void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {
Assert.notNull(convertFunc);
if (rowIndex < this.startRowIndex || rowIndex > this.endRowIndex) {
return;
}
handleData(sheetIndex, rowIndex, convertFunc.callWithRuntimeException(rowList));
handleData(sheetIndex, rowIndex, convertFunc.callWithRuntimeException(rowCells));
}
/**

View File

@ -42,11 +42,11 @@ public abstract class BeanRowHandler<T> extends AbstractRowHandler<T> {
}
@Override
public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
public void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {
if (rowIndex == this.headerRowIndex) {
this.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowList));
this.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowCells));
return;
}
super.handle(sheetIndex, rowIndex, rowList);
super.handle(sheetIndex, rowIndex, rowCells);
}
}

View File

@ -39,11 +39,11 @@ public abstract class MapRowHandler extends AbstractRowHandler<Map<String, Objec
}
@Override
public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
public void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {
if (rowIndex == this.headerRowIndex) {
this.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowList));
this.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowCells));
return;
}
super.handle(sheetIndex, rowIndex, rowList);
super.handle(sheetIndex, rowIndex, rowCells);
}
}

View File

@ -17,9 +17,9 @@ public interface RowHandler {
*
* @param sheetIndex 当前Sheet序号
* @param rowIndex 当前行号从0开始计数
* @param rowList 行数据列表
* @param rowCells 行数据每个Object表示一个单元格的值
*/
void handle(int sheetIndex, long rowIndex, List<Object> rowList);
void handle(int sheetIndex, long rowIndex, List<Object> rowCells);
/**
* 处理一个单元格的数据

View File

@ -233,4 +233,15 @@ public class ExcelReadTest {
final ExcelReader reader = ExcelUtil.getReader("d:/test/1.-.xls");
reader.read((CellHandler) Console::log);
}
@Test
public void readColumnTest(){
ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xlsx"));
final List<Object> objects = reader.readColumn(0, 1);
Assert.assertEquals(3, objects.size());
Assert.assertEquals("张三", objects.get(0));
Assert.assertEquals("李四", objects.get(1));
Assert.assertEquals("", objects.get(2));
}
}

View File

@ -125,7 +125,7 @@ public class ExcelSaxReadTest {
}
@Override
public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
public void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {
}
}
@ -143,7 +143,7 @@ public class ExcelSaxReadTest {
}
@Override
public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
public void handle(int sheetIndex, long rowIndex, List<Object> rowCells) {
}
}
);

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-script</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-setting</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-socket</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.16-SNAPSHOT</version>
<version>5.7.17-SNAPSHOT</version>
</parent>
<artifactId>hutool-system</artifactId>
@ -30,7 +30,7 @@
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>5.8.2</version>
<version>5.8.3</version>
<scope>provided</scope>
</dependency>
<dependency>

Some files were not shown because too many files have changed in this diff Show More