mirror of
https://gitee.com/dromara/hutool.git
synced 2025-04-05 17:37:59 +08:00
Merge branch 'dromara:v5-dev' into v5-dev
This commit is contained in:
commit
01c510c65e
24
CHANGELOG.md
24
CHANGELOG.md
@ -3,7 +3,27 @@
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# 5.7.16 (2021-11-04)
|
||||
# 5.7.17 (2021-11-16)
|
||||
|
||||
### 🐣新特性
|
||||
* 【core 】 增加AsyncUtil(pr#457@Gitee)
|
||||
* 【http 】 增加HttpResource(issue#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不再自动trim(issue#I4I04O@Gitee)
|
||||
* 【core 】 IdCardUtil.isValidCard不再自动trim(issue#I4I04O@Gitee)
|
||||
* 【core 】 改进TextFinder,支持限制结束位置及反向查找模式
|
||||
* 【core 】 Opt增加部分方法(pr#459@Gitee)
|
||||
* 【core 】 增加DefaultCloneable(pr#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增加sm3WithSalt(pr#454@Gitee)
|
||||
* 【http 】 增加HttpInterceptor(issue#I4H1ZV@Gitee)
|
||||
* 【core 】 Opt增加flattedMap(issue#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)
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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平台没有测试,不能保证所有工具类或工具方法可用。
|
||||
|
@ -1 +1 @@
|
||||
5.7.16
|
||||
5.7.17
|
||||
|
@ -1 +1 @@
|
||||
var version = '5.7.16'
|
||||
var version = '5.7.17'
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印
|
||||
*
|
||||
|
@ -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);
|
||||
}
|
@ -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>
|
||||
* see:https://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 / "-" / "." / "_" / "~"
|
||||
*
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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)) {
|
||||
|
@ -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&key2=&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作为name,value赋值null
|
||||
this.query.put(URLUtil.decode(value, charset), null);
|
||||
this.query.put(URLDecoder.decode(value, charset, isFormUrlEncoded), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 字符串模板
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 文本查找实现,包括:
|
||||
* <ul>
|
||||
* <li>查找文本中的字符(正向、反向)</li>
|
||||
* <li>查找文本中的匹配字符(正向、反向)</li>
|
||||
* <li>查找文本中的字符串(正向、反向)</li>
|
||||
* <li>查找文本中匹配正则的字符串(正向)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author looly
|
||||
*
|
||||
*/
|
||||
package cn.hutool.core.text.finder;
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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位身份证
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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执行的时间规则
|
||||
*
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
21
hutool-cron/src/test/java/cn/hutool/cron/TaskTableTest.java
Normal file
21
hutool-cron/src/test/java/cn/hutool/cron/TaskTableTest.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
56
hutool-http/src/main/java/cn/hutool/http/HttpResource.java
Normal file
56
hutool-http/src/main/java/cn/hutool/http/HttpResource.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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&key2=&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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
39
hutool-http/src/main/java/cn/hutool/http/body/BytesBody.java
Normal file
39
hutool-http/src/main/java/cn/hutool/http/body/BytesBody.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,4 +148,5 @@ public class HttpRequestTest {
|
||||
HttpResponse execute = get.execute();
|
||||
Console.log(execute.body());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
/**
|
||||
* 处理一个单元格的数据
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user