Merge remote-tracking branch 'upstream/v5-dev' into v5-dev

This commit is contained in:
lzpeng723 2021-01-09 19:01:38 +08:00
commit 2ea7480527
93 changed files with 1590 additions and 449 deletions

View File

@ -3,7 +3,52 @@
-------------------------------------------------------------------------------------------------------------
# 5.5.5 (2020-12-24)
# 5.5.8 (2021-01-09)
### 新特性
### Bug修复
* 【core 】 修复FileUtil.move以及PathUtil.copy等无法自动创建父目录的问题issue#I2CKTI@Gitee
-------------------------------------------------------------------------------------------------------------
# 5.5.7 (2021-01-07)
### 新特性
* 【core 】 DynaBean.create增加重载方法pr#245@Gitee
* 【core 】 IdcardUtil增加重载是否忽略大小写issue#1348@Github
* 【poi 】 SheetRidReader增加getRidByIndex方法issue#1342@Github
* 【extra 】 MailAccount增加sslProtocols配置项issue#IZN95@Gitee
* 【extra 】 MailUtil增加getSession方法
* 【setting】 新增setByGroup和putByGroupset和put标记为过期issue#I2C42H@Gitee
* 【crypto 】 修改SymmetricAlgorithm注释issue#1360@Github
* 【all 】 pom中将META-INF/maven下全部excludepr#1355@Github
* 【http 】 SimpleServer中增加addFilter等方法并使用全局线程池
* 【core 】 CollUtil.forEach 增加null 判断pr#250@Gitee
* 【extra 】 FtpConfig增加serverLanguageCode和systemKey配置,Ftp.download增加重载pr#248@Gitee
### Bug修复
* 【core 】 修复CsvReader读取双引号未转义问题issue#I2BMP1@Gitee
* 【json 】 JSONUtil.parse修复config无效问题issue#1363@Github
* 【http 】 修复SimpleServer返回响应内容Content-Length不正确的问题issue#1358@Github
* 【http 】 修复Https请求部分环境下报证书验证异常问题issue#I2C1BZ@Gitee
-------------------------------------------------------------------------------------------------------------
# 5.5.6 (2020-12-29)
### 新特性
* 【core 】 手机号工具类 座机正则表达式统一管理pr#243@Gitee
* 【extra 】 Mail增加setDebugOutput方法issue#1335@Gitee
### Bug修复
* 【core 】 修复ZipUtil.unzip从流解压关闭问题issue#I2B0S1@Gitee
* 【poi 】 修复Excel07Writer写出表格错乱问题issue#I2B57B@Gitee
* 【poi 】 修复SheetRidReader读取字段错误问题issue#1342@Github
* 【core 】 修复FileUtil.getMimeType不支持css和jsissue#1341@Github
-------------------------------------------------------------------------------------------------------------
# 5.5.5 (2020-12-27)
### 新特性
* 【core 】 URLUtil.normalize新增重载pr#233@Gitee
@ -13,14 +58,20 @@
* 【poi 】 增加ExcelDateUtil更多日期格式支持issue#1316@Github
* 【core 】 NumberUtil.toBigDecimal支持各类数字格式如1,234.56等issue#1334@Github
* 【core 】 NumberUtil增加parseXXX方法issue#1334@Github
* 【poi 】 Excel07SaxReader支持通过sheetName读取issue#I2AOSE@Gitee
### Bug修复
* 【core 】 FileUtil.isSub相对路径判断问题pr#1315@Github
* 【core 】 TreeUtil增加空判定issue#I2ACCW@Gitee
* 【db 】 解决Hive获取表名失败问题issue#I2AGLU@Gitee
* 【core 】 修复DateUtil.parse未使用严格模式导致结果不正常的问题issue#1332@Github
* 【core 】 修复RuntimeUtil.getUsableMemory非static问题issue#I2AQ2M@Gitee
* 【core 】 修复ArrayUtil.equals方法严格判断问题issue#I2AO8B@Gitee
* 【poi 】 修复SheetRidReader在获取rid时读取错误问题issue#I2AOQW@Gitee
* 【core 】 修复强依赖了POI的问题issue#1336@Github
-------------------------------------------------------------------------------------------------------------
# 5.5.4 (2020-12-16)
### 新特性

View File

@ -125,19 +125,19 @@ Each module can be introduced individually, or all modules can be introduced by
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.5</version>
<version>5.5.8</version>
</dependency>
```
### Gradle
```
compile 'cn.hutool:hutool-all:5.5.5'
compile 'cn.hutool:hutool-all:5.5.8'
```
## Download
- [Maven1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.5/)
- [Maven2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.5.5/)
- [Maven1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.8/)
- [Maven2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.5.8/)
> note:
> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.

View File

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

View File

@ -1 +1 @@
5.5.5
5.5.8

View File

@ -1 +1 @@
var version = '5.5.5'
var version = '5.5.8'

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-all</artifactId>
@ -130,6 +130,14 @@
<include>${project.groupId}:*:*</include>
</includes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/maven/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-aop</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-bloomFilter</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-bom</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-cache</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-captcha</artifactId>

View File

@ -5,19 +5,11 @@
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-core</artifactId>

View File

@ -22,21 +22,32 @@ public class DynaBean extends CloneSupport<DynaBean> implements Serializable {
private final Object bean;
/**
* 创建一个{@link DynaBean}
* 创建一个DynaBean
*
* @param bean 普通Bean
* @return {@link DynaBean}
* @return DynaBean
*/
public static DynaBean create(Object bean) {
return new DynaBean(bean);
}
/**
* 创建一个{@link DynaBean}
* 创建一个DynaBean
*
* @param beanClass Bean类
* @return DynaBean
*/
public static DynaBean create(Class<?> beanClass) {
return new DynaBean(beanClass);
}
/**
* 创建一个DynaBean
*
* @param beanClass Bean类
* @param params 构造Bean所需要的参数
* @return {@link DynaBean}
* @return DynaBean
*/
public static DynaBean create(Class<?> beanClass, Object... params) {
return new DynaBean(beanClass, params);
@ -54,6 +65,15 @@ public class DynaBean extends CloneSupport<DynaBean> implements Serializable {
this(ReflectUtil.newInstance(beanClass, params));
}
/**
* 构造
*
* @param beanClass Bean类
*/
public DynaBean(Class<?> beanClass) {
this(ReflectUtil.newInstance(beanClass));
}
/**
* 构造
*
@ -83,7 +103,7 @@ public class DynaBean extends CloneSupport<DynaBean> implements Serializable {
return (T) ((Map<?, ?>) bean).get(fieldName);
} else {
final PropDesc prop = BeanUtil.getBeanDesc(beanClass).getProp(fieldName);
if(null == prop){
if (null == prop) {
throw new BeanException("No public field or get method for {}", fieldName);
}
return (T) prop.getValue(bean);
@ -97,7 +117,7 @@ public class DynaBean extends CloneSupport<DynaBean> implements Serializable {
* @return 是否有bean属性
* @since 5.4.2
*/
public boolean containsProp(String fieldName){
public boolean containsProp(String fieldName) {
return null != BeanUtil.getBeanDesc(beanClass).getProp(fieldName);
}
@ -130,7 +150,7 @@ public class DynaBean extends CloneSupport<DynaBean> implements Serializable {
((Map) bean).put(fieldName, value);
} else {
final PropDesc prop = BeanUtil.getBeanDesc(beanClass).getProp(fieldName);
if(null == prop){
if (null == prop) {
throw new BeanException("No public field or set method for {}", fieldName);
}
prop.setValue(bean, value);

View File

@ -2560,6 +2560,9 @@ public class CollUtil {
* @since 5.4.7
*/
public static <T> void forEach(Iterable<T> iterable, Consumer<T> consumer) {
if(iterable == null){
return;
}
forEach(iterable.iterator(), consumer);
}
@ -2571,6 +2574,9 @@ public class CollUtil {
* @param consumer {@link Consumer} 遍历的每条数据处理器
*/
public static <T> void forEach(Iterator<T> iterator, Consumer<T> consumer) {
if(iterator == null){
return;
}
int index = 0;
while (iterator.hasNext()) {
consumer.accept(iterator.next(), index);
@ -2586,6 +2592,9 @@ public class CollUtil {
* @param consumer {@link Consumer} 遍历的每条数据处理器
*/
public static <T> void forEach(Enumeration<T> enumeration, Consumer<T> consumer) {
if(enumeration == null){
return;
}
int index = 0;
while (enumeration.hasMoreElements()) {
consumer.accept(enumeration.nextElement(), index);
@ -2603,6 +2612,9 @@ public class CollUtil {
* @param kvConsumer {@link KVConsumer} 遍历的每条数据处理器
*/
public static <K, V> void forEach(Map<K, V> map, KVConsumer<K, V> kvConsumer) {
if(map == null){
return;
}
int index = 0;
for (Entry<K, V> entry : map.entrySet()) {
kvConsumer.accept(entry.getKey(), entry.getValue(), index);

View File

@ -1,11 +1,12 @@
package cn.hutool.core.io;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
/**
* {@link ByteBuffer} 工具类<br>
* 此工具来自于 t-io 项目以及其它项目的相关部分收集<br>
@ -13,7 +14,6 @@ import cn.hutool.core.util.StrUtil;
*
* @author tanyaowu, looly
* @since 4.0.0
*
*/
public class BufferUtil {
@ -248,4 +248,15 @@ public class BufferUtil {
public static ByteBuffer createUtf8(CharSequence data) {
return create(StrUtil.utf8Bytes(data));
}
/**
* 创建{@link CharBuffer}
*
* @param capacity 容量
* @return {@link CharBuffer}
* @since 5.5.7
*/
public static CharBuffer createCharBuffer(int capacity) {
return CharBuffer.allocate(capacity);
}
}

View File

@ -638,12 +638,10 @@ public class FileUtil extends PathUtil {
* @return 父目录
*/
public static File mkParentDirs(File file) {
final File parentFile = file.getParentFile();
if (null != parentFile && false == parentFile.exists()) {
//noinspection ResultOfMethodCallIgnored
parentFile.mkdirs();
if(null == file){
return null;
}
return parentFile;
return mkdir(file.getParentFile());
}
/**
@ -1022,6 +1020,7 @@ public class FileUtil extends PathUtil {
* @param isOverride 是否覆盖目标文件
* @return 目标文件
* @since 3.0.9
* @see PathUtil#rename(Path, String, boolean)
*/
public static File rename(File file, String newName, boolean isRetainExt, boolean isOverride) {
if (isRetainExt) {
@ -2914,7 +2913,21 @@ public class FileUtil extends PathUtil {
* @throws IORuntimeException IO异常
*/
public static File writeFromStream(InputStream in, File dest) throws IORuntimeException {
return FileWriter.create(dest).writeFromStream(in);
return writeFromStream(in, dest, true);
}
/**
* 将流的内容写入文件
*
* @param dest 目标文件
* @param in 输入流
* @param isCloseIn 是否关闭输入流
* @return dest
* @throws IORuntimeException IO异常
* @since 5.5.6
*/
public static File writeFromStream(InputStream in, File dest, boolean isCloseIn) throws IORuntimeException {
return FileWriter.create(dest).writeFromStream(in, isCloseIn);
}
/**
@ -3169,7 +3182,22 @@ public class FileUtil extends PathUtil {
* @since 4.1.15
*/
public static String getMimeType(String filePath) {
return URLConnection.getFileNameMap().getContentTypeFor(filePath);
String contentType = URLConnection.getFileNameMap().getContentTypeFor(filePath);
if(null == contentType){
// 补充一些常用的mimeType
if(filePath.endsWith(".css")){
contentType = "text/css";
} else if(filePath.endsWith(".js")){
contentType = "application/x-javascript";
}
}
// 补充
if(null == contentType){
contentType = getMimeType(Paths.get(filePath));
}
return contentType;
}
/**

View File

@ -153,7 +153,8 @@ public class PathUtil {
}
/**
* 通过JDK7+ {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件
* 通过JDK7+ {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件<br>
* 此方法不支持递归拷贝目录如果src传入是目录只会在目标目录中创建空目录
*
* @param src 源文件路径如果为目录只在目标中创建新目录
* @param dest 目标文件或目录如果为目录使用与源文件相同的文件名
@ -166,7 +167,8 @@ public class PathUtil {
}
/**
* 通过JDK7+ {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件
* 通过JDK7+ {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件<br>
* 此方法不支持递归拷贝目录如果src传入是目录只会在目标目录中创建空目录
*
* @param src 源文件路径如果为目录只在目标中创建新目录
* @param target 目标文件或目录如果为目录使用与源文件相同的文件名
@ -180,6 +182,8 @@ public class PathUtil {
Assert.notNull(target, "Destination File or directiory is null !");
final Path targetPath = isDirectory(target) ? target.resolve(src.getFileName()) : target;
// 创建级联父目录
mkParentDirs(targetPath);
try {
return Files.copy(src, targetPath, options);
} catch (IOException e) {
@ -188,9 +192,15 @@ public class PathUtil {
}
/**
* 拷贝文件或目录
* 拷贝文件或目录拷贝规则为
*
* @param src 源文件路径如果为目录只在目标中创建新目录
* <ul>
* <li>源文件为目录目标也为目录或不存在则拷贝整个目录到目标目录下</li>
* <li>源文件为文件目标为目录或不存在则拷贝文件到目标目录下</li>
* <li>源文件为文件目标也为文件则在{@link StandardCopyOption#REPLACE_EXISTING}情况下覆盖之</li>
* </ul>
*
* @param src 源文件路径如果为目录会在目标中创建新目录
* @param target 目标文件或目录如果为目录使用与源文件相同的文件名
* @param options {@link StandardCopyOption}
* @return Path
@ -198,17 +208,21 @@ public class PathUtil {
* @since 5.5.1
*/
public static Path copy(Path src, Path target, CopyOption... options) throws IORuntimeException {
if (isFile(src, false)) {
return copyFile(src, target, options);
}
if (isDirectory(src)) {
return copyContent(src, target.resolve(src.getFileName()), options);
}
return copyFile(src, target, options);
}
/**
* 拷贝目录下的所有文件或目录到目标目录中
* 拷贝目录下的所有文件或目录到目标目录中此方法不支持文件对文件的拷贝
* <ul>
* <li>源文件为目录目标也为目录或不存在则拷贝目录下所有文件和目录到目标目录下</li>
* <li>源文件为文件目标为目录或不存在则拷贝文件到目标目录下</li>
* </ul>
*
* @param src 源文件路径如果为目录只在目标中创建新目录
* @param target 目标文件或目录如果为目录使用与源文件相同的文件名
* @param target 目标目录如果为目录使用与源文件相同的文件名
* @param options {@link StandardCopyOption}
* @return Path
* @throws IORuntimeException IO异常
@ -450,6 +464,8 @@ public class PathUtil {
if (isDirectory(target)) {
target = target.resolve(src.getFileName());
}
// 自动创建目标的父目录
mkParentDirs(target);
try {
return Files.move(src, target, options);
} catch (IOException e) {
@ -546,6 +562,7 @@ public class PathUtil {
* @param file 文件
* @return MimeType
* @since 5.5.5
* @see Files#probeContentType(Path)
*/
public static String getMimeType(Path file) {
try {
@ -554,4 +571,33 @@ public class PathUtil {
throw new IORuntimeException(e);
}
}
/**
* 创建所给目录及其父目录
*
* @param dir 目录
* @return 目录
* @since 5.5.7
*/
public static Path mkdir(Path dir) {
if (null != dir && false == exists(dir, false)) {
try {
Files.createDirectories(dir);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
return dir;
}
/**
* 创建所给文件或目录的父目录
*
* @param path 文件或目录
* @return 父目录
* @since 5.5.7
*/
public static Path mkParentDirs(Path path) {
return mkdir(path.getParent());
}
}

View File

@ -1,5 +1,7 @@
package cn.hutool.core.io.file.visitor;
import cn.hutool.core.io.file.PathUtil;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
@ -9,17 +11,28 @@ import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
/**
* 文件拷贝的FileVisitor实现用于递归遍历拷贝目录
* 文件拷贝的FileVisitor实现用于递归遍历拷贝目录此类非线程安全<br>
* 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录
*
* @author looly
* @since 5.5.1
*/
public class CopyVisitor extends SimpleFileVisitor<Path> {
final Path source;
final Path target;
private final Path source;
private final Path target;
private boolean isTargetCreated;
/**
* 构造
*
* @param source 源Path
* @param target 目标Path
*/
public CopyVisitor(Path source, Path target) {
if(PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)){
throw new IllegalArgumentException("Target must be a directory");
}
this.source = source;
this.target = target;
}
@ -27,11 +40,13 @@ public class CopyVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
initTarget();
// 将当前目录相对于源路径转换为相对于目标路径
final Path targetDir = target.resolve(source.relativize(dir));
try {
Files.copy(dir, targetDir);
} catch (FileAlreadyExistsException e) {
if (!Files.isDirectory(targetDir))
if (false == Files.isDirectory(targetDir))
throw e;
}
return FileVisitResult.CONTINUE;
@ -40,7 +55,18 @@ public class CopyVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
initTarget();
Files.copy(file, target.resolve(source.relativize(file)));
return FileVisitResult.CONTINUE;
}
/**
* 初始化目标文件或目录
*/
private void initTarget(){
if(false == this.isTargetCreated){
PathUtil.mkdir(this.target);
this.isTargetCreated = true;
}
}
}

View File

@ -56,7 +56,10 @@ public class PatternPool {
* 移动电话
*/
public final static Pattern MOBILE = Pattern.compile("(?:0|86|\\+86)?1[3-9]\\d{9}");
/**
* 座机号码
*/
public final static Pattern TEL = Pattern.compile("0\\d{2,3}-[1-9]\\d{6,7}");
/**
* 18位身份证号码
*/

View File

@ -1,12 +1,13 @@
package cn.hutool.http.ssl;
package cn.hutool.core.net;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
* 证书管理
* 默认信任管理器默认信任所有客户端和服务端证书
*
* @author Looly
* @since 5.5.7
*/
public class DefaultTrustManager implements X509TrustManager {

View File

@ -10,6 +10,7 @@ import cn.hutool.core.util.StrUtil;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.DatagramSocket;
import java.net.HttpCookie;
import java.net.IDN;
@ -76,6 +77,39 @@ public class NetUtil {
return Ipv4Util.ipv4ToLong(strIP);
}
/**
* 将IPv6地址字符串转为大整数
*
* @param IPv6Str 字符串
* @return 大整数, 如发生异常返回 null
* @since 5.5.7
*/
public static BigInteger ipv6ToBitInteger(String IPv6Str) {
try {
InetAddress address = InetAddress.getByName(IPv6Str);
if (address instanceof Inet6Address) {
return new BigInteger(1, address.getAddress());
}
} catch (UnknownHostException ignore) {
}
return null;
}
/**
* 将大整数转换成ipv6字符串
*
* @param bigInteger 大整数
* @return IPv6字符串, 如发生异常返回 null
* @since 5.5.7
*/
public static String bigIntegerToIPv6(BigInteger bigInteger) {
try {
return InetAddress.getByAddress(bigInteger.toByteArray()).toString().substring(1);
} catch (UnknownHostException ignore) {
return null;
}
}
/**
* 检测本地端口可用性<br>
* 来自org.springframework.util.SocketUtils
@ -512,7 +546,7 @@ public class NetUtil {
byte[] mac = null;
try {
final NetworkInterface networkInterface = NetworkInterface.getByInetAddress(inetAddress);
if(null != networkInterface){
if (null != networkInterface) {
mac = networkInterface.getHardwareAddress();
}
} catch (SocketException e) {
@ -547,9 +581,9 @@ public class NetUtil {
}
final InetAddress localhost = getLocalhost();
if(null != localhost){
if (null != localhost) {
String name = localhost.getHostName();
if(StrUtil.isEmpty(name)){
if (StrUtil.isEmpty(name)) {
name = localhost.getHostAddress();
}
localhostName = name;
@ -738,7 +772,7 @@ public class NetUtil {
* @since 5.3.2
*/
public static boolean isOpen(InetSocketAddress address, int timeout) {
try (Socket sc = new Socket()){
try (Socket sc = new Socket()) {
sc.connect(address, timeout);
return true;
} catch (Exception e) {

View File

@ -52,7 +52,7 @@ public class SSLContextBuilder {
private String protocol = TLS;
private KeyManager[] keyManagers;
private TrustManager[] trustManagers;
private TrustManager[] trustManagers = {new DefaultTrustManager()};
private SecureRandom secureRandom = new SecureRandom();

View File

@ -1,22 +1,30 @@
package cn.hutool.core.text.csv;
import java.io.Serializable;
import cn.hutool.core.util.CharUtil;
import java.io.Serializable;
/**
* CSV基础配置项
* CSV基础配置项此配置项可用于读取和写出CSV定义了包括字段分隔符文本包装符等符号
*
* @author looly
* @since 4.0.5
*/
public class CsvConfig implements Serializable{
public class CsvConfig implements Serializable {
private static final long serialVersionUID = -8069578249066158459L;
/** 字段分隔符,默认逗号',' */
/**
* 字段分隔符默认逗号','
*/
protected char fieldSeparator = CharUtil.COMMA;
/** 文本分隔符,文本包装符,默认双引号'"' */
/**
* 文本包装符默认双引号'"'
*/
protected char textDelimiter = CharUtil.DOUBLE_QUOTES;
/**
* 注释符号用于区分注释行默认'#'
*/
protected char commentCharacter = '#';
/**
* 设置字段分隔符默认逗号','
@ -35,4 +43,14 @@ public class CsvConfig implements Serializable{
public void setTextDelimiter(char textDelimiter) {
this.textDelimiter = textDelimiter;
}
/**
* 设置 注释符号用于区分注释行
*
* @param commentCharacter 注释符号用于区分注释行
* @since 5.5.7
*/
public void setCommentCharacter(char commentCharacter) {
this.commentCharacter = commentCharacter;
}
}

View File

@ -31,29 +31,39 @@ public final class CsvParser implements Closeable, Serializable {
private final Reader reader;
private final CsvReadConfig config;
private final char[] buf = new char[IoUtil.DEFAULT_LARGE_BUFFER_SIZE];
/** 当前位置 */
private int bufPos;
/** 读取一段后数据长度 */
private int bufLen;
/** 拷贝开始的位置,一般为上一行的结束位置 */
private int copyStart;
/** 前一个特殊分界字符 */
private final Buffer buf = new Buffer(IoUtil.DEFAULT_LARGE_BUFFER_SIZE);
/**
* 前一个特殊分界字符
*/
private int preChar = -1;
/** 是否在引号包装内 */
/**
* 是否在引号包装内
*/
private boolean inQuotes;
/** 当前读取字段 */
/**
* 当前读取字段
*/
private final StrBuilder currentField = new StrBuilder(512);
/** 标题行 */
/**
* 标题行
*/
private CsvRow header;
/** 当前行号 */
/**
* 当前行号
*/
private long lineNo;
/** 第一行字段数,用于检查每行字段数是否一致 */
/**
* 第一行字段数用于检查每行字段数是否一致
*/
private int firstLineFieldCount = -1;
/** 最大字段数量 */
/**
* 最大字段数量用于初始化行减少扩容
*/
private int maxFieldCount;
/** 是否读取结束 */
/**
* 是否读取结束
*/
private boolean finished;
/**
@ -84,7 +94,7 @@ public final class CsvParser implements Closeable, Serializable {
}
/**
*读取下一行数据
* 读取下一行数据
*
* @return CsvRow
* @throws IORuntimeException IO读取异常
@ -97,7 +107,7 @@ public final class CsvParser implements Closeable, Serializable {
startingLineNo = ++lineNo;
currentFields = readLine();
fieldCount = currentFields.size();
if(fieldCount < 1){
if (fieldCount < 1) {
break;
}
@ -142,7 +152,7 @@ public final class CsvParser implements Closeable, Serializable {
final Map<String, Integer> localHeaderMap = new LinkedHashMap<>(currentFields.size());
for (int i = 0; i < currentFields.size(); i++) {
final String field = currentFields.get(i);
if (StrUtil.isNotEmpty(field) && false ==localHeaderMap.containsKey(field)) {
if (StrUtil.isNotEmpty(field) && false == localHeaderMap.containsKey(field)) {
localHeaderMap.put(field, i);
}
}
@ -159,42 +169,55 @@ public final class CsvParser implements Closeable, Serializable {
private List<String> readLine() throws IORuntimeException {
final List<String> currentFields = new ArrayList<>(maxFieldCount > 0 ? maxFieldCount : DEFAULT_ROW_CAPACITY);
final StrBuilder localCurrentField = currentField;
final char[] localBuf = this.buf;
int localBufPos = bufPos;//当前位置
int localPreChar = preChar;//前一个特殊分界字符
int localCopyStart = copyStart;//拷贝起始位置
final StrBuilder currentField = this.currentField;
final Buffer buf = this.buf;
int preChar = this.preChar;//前一个特殊分界字符
int copyLen = 0; //拷贝长度
boolean lineStart = true;
boolean inComment = false;
while (true) {
if (bufLen == localBufPos) {
if (false == buf.hasRemaining()) {
// 此Buffer读取结束开始读取下一段
if (copyLen > 0) {
localCurrentField.append(localBuf, localCopyStart, copyLen);
buf.appendTo(currentField, copyLen);
// 此处无需markread方法会重置mark
}
try {
bufLen = reader.read(localBuf);
} catch (IOException e) {
throw new IORuntimeException(e);
}
if (bufLen < 0) {
if (buf.read(this.reader) < 0) {
// CSV读取结束
finished = true;
if (localPreChar == config.fieldSeparator || localCurrentField.hasContent()) {
if (currentField.hasContent() || preChar == config.fieldSeparator) {
//剩余部分作为一个字段
currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter));
addField(currentFields, currentField.toStringAndReset());
}
break;
}
//重置
localCopyStart = localBufPos = copyLen = 0;
copyLen = 0;
}
final char c = localBuf[localBufPos++];
final char c = buf.get();
// 注释行标记
if(lineStart){
if(c == this.config.commentCharacter){
inComment = true;
}
lineStart = false;
}
// 注释行处理
if(inComment){
if ((c == CharUtil.CR || c == CharUtil.LF) && preChar != CharUtil.CR) {
// 注释行以换行符为结尾
inComment = false;
}
// 跳过注释行中的任何字符
buf.mark();
preChar = c;
continue;
}
if (inQuotes) {
//引号内做为内容直到引号结束
@ -202,55 +225,60 @@ public final class CsvParser implements Closeable, Serializable {
// End of quoted text
inQuotes = false;
} else {
if ((c == CharUtil.CR || c == CharUtil.LF) && localPreChar != CharUtil.CR) {
// 新行
if ((c == CharUtil.CR || c == CharUtil.LF) && preChar != CharUtil.CR) {
lineNo++;
}
}
// 普通字段字符
copyLen++;
} else {
// 非引号内
if (c == config.fieldSeparator) {
//一个字段结束
if (copyLen > 0) {
localCurrentField.append(localBuf, localCopyStart, copyLen);
buf.appendTo(currentField, copyLen);
copyLen = 0;
}
currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter));
localCopyStart = localBufPos;
buf.mark();
addField(currentFields, currentField.toStringAndReset());
} else if (c == config.textDelimiter) {
// 引号开始
inQuotes = true;
copyLen++;
} else if (c == CharUtil.CR) {
// \r直接结束
if (copyLen > 0) {
localCurrentField.append(localBuf, localCopyStart, copyLen);
buf.appendTo(currentField, copyLen);
}
currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter));
localPreChar = c;
localCopyStart = localBufPos;
buf.mark();
addField(currentFields, currentField.toStringAndReset());
preChar = c;
break;
} else if (c == CharUtil.LF) {
if (localPreChar != CharUtil.CR) {
// \n
if (preChar != CharUtil.CR) {
if (copyLen > 0) {
localCurrentField.append(localBuf, localCopyStart, copyLen);
buf.appendTo(currentField, copyLen);
}
currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter));
localPreChar = c;
localCopyStart = localBufPos;
buf.mark();
addField(currentFields, currentField.toStringAndReset());
preChar = c;
break;
}
localCopyStart = localBufPos;
// 前一个字符是\r已经处理过这个字段了此处直接跳过
buf.mark();
} else {
// 普通字符
copyLen++;
}
}
localPreChar = c;
preChar = c;
}
// restore fields
bufPos = localBufPos;
preChar = localPreChar;
copyStart = localCopyStart;
this.preChar = preChar;
return currentFields;
}
@ -259,4 +287,99 @@ public final class CsvParser implements Closeable, Serializable {
public void close() throws IOException {
reader.close();
}
/**
* 将字段加入字段列表并自动去包装和去转义
*
* @param currentFields 当前的字段列表即为行
* @param field 字段
*/
private void addField(List<String> currentFields, String field) {
field = StrUtil.unWrap(field, config.textDelimiter);
char textDelimiter = this.config.textDelimiter;
field = StrUtil.replace(field, "" + textDelimiter + textDelimiter, textDelimiter + "");
currentFields.add(StrUtil.unWrap(field, textDelimiter));
}
/**
* 内部Buffer
*
* @author looly
*/
private static class Buffer {
final char[] buf;
/**
* 标记位置用于读数据
*/
private int mark;
/**
* 当前位置
*/
private int position;
/**
* 读取的数据长度一般小于buf.length-1表示无数据
*/
private int limit;
Buffer(int capacity) {
buf = new char[capacity];
}
/**
* 是否还有未读数据
*
* @return 是否还有未读数据
*/
public final boolean hasRemaining() {
return position < limit;
}
/**
* 读取到缓存
*
* @param reader {@link Reader}
*/
int read(Reader reader) {
int length;
try {
length = reader.read(this.buf);
} catch (IOException e) {
throw new IORuntimeException(e);
}
this.mark = 0;
this.position = 0;
this.limit = length;
return length;
}
/**
* 先获取当前字符再将当前位置后移一位<br>
* 此方法不检查是否到了数组末尾请自行使用{@link #hasRemaining()}判断
*
* @return 当前位置字符
* @see #hasRemaining()
*/
char get() {
return this.buf[this.position++];
}
/**
* 标记位置记为下次读取位置
*/
void mark() {
this.mark = this.position;
}
/**
* 将数据追加到{@link StrBuilder}追加结束后需手动调用{@link #mark()} 重置读取位置
*
* @param builder {@link StrBuilder}
* @param length 追加的长度
* @see #mark()
*/
void appendTo(StrBuilder builder, int length) {
builder.append(this.buf, this.mark, length);
}
}
}

View File

@ -29,14 +29,21 @@ import java.util.Collection;
public final class CsvWriter implements Closeable, Flushable, Serializable {
private static final long serialVersionUID = 1L;
/** 写出器 */
/**
* 写出器
*/
private final Writer writer;
/** 写出配置 */
/**
* 写出配置
*/
private final CsvWriteConfig config;
/** 是否处于新行开始 */
/**
* 是否处于新行开始
*/
private boolean newline = true;
// --------------------------------------------------------------------------------------------------- Constructor start
/**
* 构造覆盖已有文件如果存在默认编码UTF-8
*
@ -198,18 +205,56 @@ public final class CsvWriter implements Closeable, Flushable, Serializable {
return this;
}
/**
* 写出一行
*
* @param fields 字段列表 ({@code null} 值会被做为空值追加)
* @return this
* @throws IORuntimeException IO异常
* @since 5.5.7
*/
public CsvWriter writeLine(String... fields) throws IORuntimeException {
if (ArrayUtil.isEmpty(fields)) {
return writeLine();
}
appendLine(fields);
return this;
}
/**
* 追加新行换行
*
* @return this
* @throws IORuntimeException IO异常
*/
public void writeLine() throws IORuntimeException {
public CsvWriter writeLine() throws IORuntimeException {
try {
writer.write(config.lineDelimiter);
} catch (IOException e) {
throw new IORuntimeException(e);
}
newline = true;
return this;
}
/**
* 写出一行注释注释符号可自定义
*
* @param comment 注释内容
* @return this
* @see CsvConfig#commentCharacter
* @since 5.5.7
*/
public CsvWriter writeComment(String comment) {
try {
writer.write(this.config.commentCharacter);
writer.write(comment);
writer.write(config.lineDelimiter);
newline = true;
} catch (IOException e) {
throw new IORuntimeException(e);
}
return this;
}
@Override
@ -227,13 +272,14 @@ public final class CsvWriter implements Closeable, Flushable, Serializable {
}
// --------------------------------------------------------------------------------------------------- Private method start
/**
* 追加一行末尾会自动换行但是追加前不会换行
*
* @param fields 字段列表 ({@code null} 值会被做为空值追加)
* @throws IORuntimeException IO异常
*/
private void appendLine(final String... fields) throws IORuntimeException {
private void appendLine(String... fields) throws IORuntimeException {
try {
doAppendLine(fields);
} catch (IOException e) {
@ -276,7 +322,7 @@ public final class CsvWriter implements Closeable, Flushable, Serializable {
if (null == value) {
if (alwaysDelimitText) {
writer.write(new char[] { textDelimiter, textDelimiter });
writer.write(new char[]{textDelimiter, textDelimiter});
}
return;
}

View File

@ -523,7 +523,7 @@ public class ArrayUtil extends PrimitiveArrayUtil {
}
/**
* 克隆数组如果非数组返回<code>null</code>
* 克隆数组如果非数组返回{@code null}
*
* @param <T> 数组元素类型
* @param obj 数组对象
@ -1667,11 +1667,6 @@ public class ArrayUtil extends PrimitiveArrayUtil {
Assert.isTrue(isArray(array1), "First is not a Array !");
Assert.isTrue(isArray(array2), "Second is not a Array !");
// 数组类型一致性判断
if (array1.getClass() != array2.getClass()) {
return false;
}
if (array1 instanceof long[]) {
return Arrays.equals((long[]) array1, (long[]) array2);
} else if (array1 instanceof int[]) {

View File

@ -47,7 +47,7 @@ public class CharUtil {
public static final char AMP = '&';
/** 字符常量:冒号 {@code ':'} */
public static final char COLON = ':';
/** 字符常量:艾特 <code>'@'</code> */
/** 字符常量:艾特 {@code '@'} */
public static final char AT = '@';
/**

View File

@ -147,7 +147,7 @@ public class IdcardUtil {
}
/**
* 是否有效身份证号
* 是否有效身份证号忽略X的大小写
*
* @param idCard 身份证号支持18位15位和港澳台的10位
* @return 是否有效
@ -198,9 +198,46 @@ public class IdcardUtil {
* </ol>
*
* @param idcard 待验证的身份证
* @return 是否有效的18位身份证
* @return 是否有效的18位身份证忽略x的大小写
*/
public static boolean isValidCard18(String idcard) {
return isValidCard18(idcard, true);
}
/**
* <p>
* 判断18位身份证的合法性
* </p>
* 根据中华人民共和国国家标准GB11643-1999中有关公民身份号码的规定公民身份号码是特征组合码由十七位数字本体码和一位数字校验码组成<br>
* 排列顺序从左至右依次为六位数字地址码八位数字出生日期码三位数字顺序码和一位数字校验码
* <p>
* 顺序码: 表示在同一地址码所标识的区域范围内对同年同月 日出生的人编定的顺序号顺序码的奇数分配给男性偶数分配 给女性
* </p>
* <ol>
* <li>第12位数字表示所在省份的代码</li>
* <li>第34位数字表示所在城市的代码</li>
* <li>第56位数字表示所在区县的代码</li>
* <li>第7~14位数字表示出生年</li>
* <li>第1516位数字表示所在地的派出所的代码</li>
* <li>第17位数字表示性别奇数表示男性偶数表示女性</li>
* <li>第18位数字是校检码用来检验身份证的正确性校检码可以是0~9的数字有时也用x表示</li>
* </ol>
* <p>
* 第十八位数字(校验码)的计算方法为
* <ol>
* <li>将前面的身份证号码17位数分别乘以不同的系数从第一位到第十七位的系数分别为7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2</li>
* <li>将这17位数字和系数相乘的结果相加</li>
* <li>用加出来和除以11看余数是多少</li>
* <li>余数只可能有0 1 2 3 4 5 6 7 8 9 10这11个数字其分别对应的最后一位身份证的号码为1 0 X 9 8 7 6 5 4 3 2</li>
* <li>通过上面得知如果余数是2就会在身份证的第18位数字上出现罗马数字的如果余数是10身份证的最后一位号码就是2</li>
* </ol>
*
* @param idcard 待验证的身份证
* @param ignoreCase 是否忽略大小写{@code true}则忽略X大小写否则严格匹配大写
* @return 是否有效的18位身份证
* @since 5.5.7
*/
public static boolean isValidCard18(String idcard, boolean ignoreCase) {
if (CHINA_ID_MAX_LENGTH != idcard.length()) {
return false;
}
@ -217,13 +254,12 @@ public class IdcardUtil {
}
// 前17位
String code17 = idcard.substring(0, 17);
// 第18位
char code18 = Character.toLowerCase(idcard.charAt(17));
final String code17 = idcard.substring(0, 17);
if (ReUtil.isMatch(PatternPool.NUMBERS, code17)) {
// 获取校验位
char val = getCheckCode18(code17);
return val == code18;
// 第18位
return CharUtil.equals(val, idcard.charAt(17), ignoreCase);
}
return false;
}
@ -586,7 +622,7 @@ public class IdcardUtil {
case 3:
return '9';
case 2:
return 'x';
return 'X';
case 1:
return '0';
case 0:

View File

@ -14,11 +14,6 @@ import java.util.regex.Pattern;
*/
public class PhoneUtil {
/**
* 座机号码
*/
private static final Pattern TEL = Pattern.compile("0\\d{2,3}-[1-9]\\d{6,7}");
/**
* 验证是否为手机号码中国
*
@ -38,7 +33,7 @@ public class PhoneUtil {
* @since 5.3.11
*/
public static boolean isTel(CharSequence value) {
return Validator.isMatchRegex(TEL, value);
return Validator.isMatchRegex(PatternPool.TEL, value);
}
/**

View File

@ -298,7 +298,7 @@ public class RuntimeUtil {
*
* @return 最大可用内存
*/
public final long getUsableMemory() {
public static long getUsableMemory() {
return getMaxMemory() - getTotalMemory() + getFreeMemory();
}
}

View File

@ -644,7 +644,7 @@ public class ZipUtil {
outItemFile.mkdirs();
} else {
// 文件
FileUtil.writeFromStream(zipStream, outItemFile);
FileUtil.writeFromStream(zipStream, outItemFile, false);
}
});
return outFile;

View File

@ -1,19 +1,18 @@
package cn.hutool.core.bean;
import lombok.Data;
import org.junit.Assert;
import org.junit.Test;
import cn.hutool.core.bean.DynaBean;
/**
* {@link DynaBean}单元测试
* @author Looly
*
* @author Looly
*/
public class DynaBeanTest {
@Test
public void beanTest(){
public void beanTest() {
User user = new User();
DynaBean bean = DynaBean.create(user);
bean.set("name", "李华");
@ -34,29 +33,63 @@ public class DynaBeanTest {
Assert.assertEquals("test for 李华", invoke);
}
public static class User{
@Test
public void beanByStaticClazzConstructorTest() {
String name_before = "李华";
int age_before = 12;
DynaBean bean = DynaBean.create(User.class);
bean.set("name", name_before);
bean.set("age", age_before);
String name_after = bean.get("name");
Assert.assertEquals(name_before, name_after);
int age_after = bean.get("age");
Assert.assertEquals(age_before, age_after);
//重复包装测试
DynaBean bean2 = new DynaBean(bean);
User user2 = bean2.getBean();
User user1 = bean.getBean();
Assert.assertEquals(user1, user2);
//执行指定方法
Object invoke = bean2.invoke("testMethod");
Assert.assertEquals("test for 李华", invoke);
}
@Test
public void beanByInstanceClazzConstructorTest() {
String name_before = "李华";
int age_before = 12;
DynaBean bean = new DynaBean(User.class);
bean.set("name", name_before);
bean.set("age", age_before);
String name_after = bean.get("name");
Assert.assertEquals(name_before, name_after);
int age_after = bean.get("age");
Assert.assertEquals(age_before, age_after);
//重复包装测试
DynaBean bean2 = new DynaBean(bean);
User user2 = bean2.getBean();
User user1 = bean.getBean();
Assert.assertEquals(user1, user2);
//执行指定方法
Object invoke = bean2.invoke("testMethod");
Assert.assertEquals("test for 李华", invoke);
}
@Data
public static class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String testMethod(){
public String testMethod() {
return "test for " + this.name;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}
}

View File

@ -376,6 +376,15 @@ public class FileUtilTest {
public void getMimeTypeTest() {
String mimeType = FileUtil.getMimeType("test2Write.jpg");
Assert.assertEquals("image/jpeg", mimeType);
mimeType = FileUtil.getMimeType("test2Write.html");
Assert.assertEquals("text/html", mimeType);
mimeType = FileUtil.getMimeType("main.css");
Assert.assertEquals("text/css", mimeType);
mimeType = FileUtil.getMimeType("test.js");
Assert.assertEquals("application/x-javascript", mimeType);
}
@Test

View File

@ -25,7 +25,16 @@ public class PathUtilTest {
public void copyTest(){
PathUtil.copy(
Paths.get("d:/Red2_LYY"),
Paths.get("d:/test/")
Paths.get("d:/test/aaa/aaa.txt")
);
}
@Test
@Ignore
public void copyContentTest(){
PathUtil.copyContent(
Paths.get("d:/Red2_LYY"),
Paths.get("d:/test/aaa/")
);
}
@ -37,7 +46,10 @@ public class PathUtilTest {
@Test
public void getMimeTypeTest(){
final String mimeType = PathUtil.getMimeType(Paths.get("d:/test/test.jpg"));
String mimeType = PathUtil.getMimeType(Paths.get("d:/test/test.jpg"));
Assert.assertEquals("image/jpeg", mimeType);
mimeType = PathUtil.getMimeType(Paths.get("d:/test/test.mov"));
Assert.assertEquals("video/quicktime", mimeType);
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.thread.ConcurrencyTester;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
@ -66,4 +67,12 @@ public class SnowflakeTest {
}
});
}
@Test
public void getSnowflakeLengthTest(){
for (int i = 0; i < 1000; i++) {
final long l = IdUtil.getSnowflake(0, 0).nextId();
Assert.assertEquals(19, StrUtil.toString(l).length());
}
}
}

View File

@ -1,8 +1,9 @@
package cn.hutool.core.text.csv;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
@ -16,21 +17,41 @@ public class CsvUtilTest {
//从文件中读取CSV数据
CsvData data = reader.read(FileUtil.file("test.csv"));
List<CsvRow> rows = data.getRows();
for (CsvRow csvRow : rows) {
Assert.notEmpty(csvRow.getRawList());
}
final CsvRow row0 = rows.get(0);
Assert.assertEquals("sss,sss", row0.get(0));
Assert.assertEquals("姓名", row0.get(1));
Assert.assertEquals("性别", row0.get(2));
Assert.assertEquals("关注\"对象\"", row0.get(3));
Assert.assertEquals("年龄", row0.get(4));
Assert.assertEquals("", row0.get(5));
Assert.assertEquals("\"", row0.get(6));
}
@Test
public void readTest2() {
CsvReader reader = CsvUtil.getReader();
reader.read(FileUtil.getUtf8Reader("test.csv"), (csvRow)-> Assert.notEmpty(csvRow.getRawList()));
reader.read(FileUtil.getUtf8Reader("test.csv"), (csvRow)-> {
// 只有一行所以直接判断
Assert.assertEquals("sss,sss", csvRow.get(0));
Assert.assertEquals("姓名", csvRow.get(1));
Assert.assertEquals("性别", csvRow.get(2));
Assert.assertEquals("关注\"对象\"", csvRow.get(3));
Assert.assertEquals("年龄", csvRow.get(4));
Assert.assertEquals("", csvRow.get(5));
Assert.assertEquals("\"", csvRow.get(6));
});
}
@Test
public void readTest3() {
CsvReader reader = CsvUtil.getReader();
reader.read(FileUtil.getUtf8Reader("test.csv"), Console::log);
}
@Test
@Ignore
public void writeTest() {
CsvWriter writer = CsvUtil.getWriter("e:/testWrite.csv", CharsetUtil.CHARSET_UTF_8);
CsvWriter writer = CsvUtil.getWriter("d:/test/testWrite.csv", CharsetUtil.CHARSET_UTF_8);
writer.write(
new String[] {"a1", "b1", "c1", "123345346456745756756785656"},
new String[] {"a2", "b2", "c2"},
@ -38,4 +59,13 @@ public class CsvUtilTest {
);
}
@Test
@Ignore
public void readLfTest(){
final CsvReader reader = CsvUtil.getReader();
final CsvData read = reader.read(FileUtil.file("d:/test/rw_test.csv"));
for (CsvRow row : read) {
Console.log(row);
}
}
}

View File

@ -43,7 +43,7 @@ public class IdcardUtilTest {
Assert.assertEquals("150102198807303035", convert15To18);
String convert15To18Second = IdcardUtil.convert15To18("330102200403064");
Assert.assertEquals("33010219200403064x", convert15To18Second);
Assert.assertEquals("33010219200403064X", convert15To18Second);
}
@Test
@ -89,8 +89,20 @@ public class IdcardUtilTest {
@Test
public void isValidCard18Test(){
final boolean isValidCard18 = IdcardUtil.isValidCard18("3301022011022000D6");
boolean isValidCard18 = IdcardUtil.isValidCard18("3301022011022000D6");
Assert.assertFalse(isValidCard18);
// 不忽略大小写情况下X严格校验必须大写
isValidCard18 = IdcardUtil.isValidCard18("33010219200403064x", false);
Assert.assertFalse(isValidCard18);
isValidCard18 = IdcardUtil.isValidCard18("33010219200403064X", false);
Assert.assertTrue(isValidCard18);
// 非严格校验下大小写皆可
isValidCard18 = IdcardUtil.isValidCard18("33010219200403064x");
Assert.assertTrue(isValidCard18);
isValidCard18 = IdcardUtil.isValidCard18("33010219200403064X");
Assert.assertTrue(isValidCard18);
}
@Test

View File

@ -1,11 +1,10 @@
package cn.hutool.core.util;
import cn.hutool.core.lang.Console;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.RuntimeUtil;
/**
* 命令行单元测试
* @author looly
@ -26,4 +25,9 @@ public class RuntimeUtilTest {
String str = RuntimeUtil.execForStr("cmd /c dir");
Console.log(str);
}
@Test
public void getUsableMemoryTest(){
Assert.assertTrue(RuntimeUtil.getUsableMemory() > 0);
}
}

View File

@ -1,6 +1,7 @@
package cn.hutool.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.lang.Console;
import org.junit.Assert;
import org.junit.Ignore;
@ -8,6 +9,7 @@ import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
@ -102,8 +104,9 @@ public class ZipUtilTest {
try (OutputStream out = new FileOutputStream(zip)){
//实际应用中, out HttpServletResponse.getOutputStream
ZipUtil.zip(out, Charset.defaultCharset(), false, null, new File(dir));
} catch (Exception e) {
e.printStackTrace();
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
}

View File

@ -1 +1,2 @@
"sss,sss",姓名,"性别",关注"对象",年龄
# 这是一行注释,读取时应忽略
"sss,sss",姓名,"性别",关注"对象",年龄,"","""
Can't render this file because it contains an unexpected character in line 1 and column 33.

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-cron</artifactId>

View File

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

View File

@ -8,7 +8,7 @@ package cn.hutool.crypto.symmetric;
*
*/
public enum SymmetricAlgorithm {
/** 默认的AES加密方式AES/CBC/PKCS5Padding */
/** 默认的AES加密方式AES/ECB/PKCS5Padding */
AES("AES"),
ARCFOUR("ARCFOUR"),
Blowfish("Blowfish"),

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-db</artifactId>
@ -21,12 +21,12 @@
<c3p0.version>0.9.5.5</c3p0.version>
<dbcp2.version>2.8.0</dbcp2.version>
<tomcat-jdbc.version>9.0.30</tomcat-jdbc.version>
<druid.version>1.2.3</druid.version>
<druid.version>1.2.4</druid.version>
<hikariCP.version>2.4.13</hikariCP.version>
<mongo.version>3.12.7</mongo.version>
<sqlite.version>3.32.3.2</sqlite.version>
<sqlite.version>3.34.0</sqlite.version>
<hsqldb.version>2.5.1</hsqldb.version>
<jedis.version>3.3.0</jedis.version>
<jedis.version>3.4.1</jedis.version>
</properties>
<dependencies>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-dfa</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-extra</artifactId>
@ -23,14 +23,14 @@
<rythm.version>1.3.0</rythm.version>
<freemarker.version>2.3.30</freemarker.version>
<enjoy.version>4.9.03</enjoy.version>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf.version>3.0.12.RELEASE</thymeleaf.version>
<mail.version>1.6.2</mail.version>
<jsch.version>0.1.55</jsch.version>
<zxing.version>3.4.1</zxing.version>
<net.version>3.7.2</net.version>
<emoji-java.version>5.1.1</emoji-java.version>
<servlet-api.version>4.0.1</servlet-api.version>
<spring-boot.version>2.4.0</spring-boot.version>
<spring-boot.version>2.4.1</spring-boot.version>
<cglib.version>3.3.0</cglib.version>
</properties>
@ -406,7 +406,7 @@
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.10.Final</version>
<version>2.4.11.Final</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -420,7 +420,7 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.1</version>
<version>5.3.2</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>

View File

@ -119,7 +119,7 @@ public class StreamExtractor implements Extractor{
//noinspection ResultOfMethodCallIgnored
outItemFile.mkdirs();
} else {
FileUtil.writeFromStream(in, outItemFile);
FileUtil.writeFromStream(in, outItemFile, false);
}
}
}

View File

@ -8,6 +8,7 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
@ -16,6 +17,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@ -81,7 +83,22 @@ public class Ftp extends AbstractFtp {
* @param charset 编码
*/
public Ftp(String host, int port, String user, String password, Charset charset) {
this(host, port, user, password, charset, null);
this(host, port, user, password, charset, null, null);
}
/**
* 构造
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @param charset 编码
* @param serverLanguageCode 服务器语言 例如zh
* @param systemKey 服务器标识 例如org.apache.commons.net.ftp.FTPClientConfig.SYST_NT
*/
public Ftp(String host, int port, String user, String password, Charset charset, String serverLanguageCode, String systemKey) {
this(host, port, user, password, charset, serverLanguageCode, systemKey, null);
}
/**
@ -94,8 +111,8 @@ public class Ftp extends AbstractFtp {
* @param charset 编码
* @param mode 模式
*/
public Ftp(String host, int port, String user, String password, Charset charset, FtpMode mode) {
this(new FtpConfig(host, port, user, password, charset), mode);
public Ftp(String host, int port, String user, String password, Charset charset, String serverLanguageCode, String systemKey, FtpMode mode) {
this(new FtpConfig(host, port, user, password, charset, serverLanguageCode, systemKey), mode);
}
/**
@ -143,7 +160,7 @@ public class Ftp extends AbstractFtp {
* @return this
*/
public Ftp init(String host, int port, String user, String password, FtpMode mode) {
return init(new FtpConfig(host, port, user, password, this.ftpConfig.getCharset()), mode);
return init(new FtpConfig(host, port, user, password, this.ftpConfig.getCharset(), null, null), mode);
}
/**
@ -155,8 +172,21 @@ public class Ftp extends AbstractFtp {
*/
public Ftp init(FtpConfig config, FtpMode mode) {
final FTPClient client = new FTPClient();
client.setControlEncoding(config.getCharset().toString());
final Charset charset = config.getCharset();
if (null != charset) {
client.setControlEncoding(charset.toString());
}
client.setConnectTimeout((int) config.getConnectionTimeout());
final String systemKey = config.getSystemKey();
if (StrUtil.isNotBlank(systemKey)) {
final FTPClientConfig conf = new FTPClientConfig(systemKey);
final String serverLanguageCode = config.getServerLanguageCode();
if (StrUtil.isNotBlank(serverLanguageCode)) {
conf.setServerLanguageCode(config.getServerLanguageCode());
}
client.configure(conf);
}
try {
// 连接ftp服务器
client.connect(config.getHost(), config.getPort());
@ -344,6 +374,7 @@ public class Ftp extends AbstractFtp {
/**
* 获取服务端目录状态
*
* @param path 路径
* @return 状态int服务端不同返回不同
* @since 5.4.3
@ -579,12 +610,29 @@ public class Ftp extends AbstractFtp {
* @param out 输出位置
*/
public void download(String path, String fileName, OutputStream out) {
download(path, fileName, out, null);
}
/**
* 下载文件到输出流
*
* @param path 文件路径
* @param fileName 文件名
* @param out 输出位置
* @param fileNameCharset 文件名编码
* @since 5.5.7
*/
public void download(String path, String fileName, OutputStream out, Charset fileNameCharset) {
String pwd = null;
if (this.backToPwd) {
pwd = pwd();
}
cd(path);
if (null != fileNameCharset) {
fileName = new String(fileName.getBytes(fileNameCharset), StandardCharsets.ISO_8859_1);
}
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
client.retrieveFile(fileName, out);

View File

@ -11,7 +11,7 @@ import java.nio.charset.Charset;
public class FtpConfig implements Serializable {
private static final long serialVersionUID = 1L;
public static FtpConfig create(){
public static FtpConfig create() {
return new FtpConfig();
}
@ -46,6 +46,16 @@ public class FtpConfig implements Serializable {
*/
private long soTimeout;
/**
* 设置服务器语言
*/
private String serverLanguageCode;
/**
* 设置服务器系统关键词
*/
private String systemKey;
/**
* 构造
*/
@ -62,11 +72,29 @@ public class FtpConfig implements Serializable {
* @param charset 编码
*/
public FtpConfig(String host, int port, String user, String password, Charset charset) {
this(host, port, user, password, charset, null, null);
}
/**
* 构造
*
* @param host 主机
* @param port 端口
* @param user 用户名
* @param password 密码
* @param charset 编码
* @param serverLanguageCode 服务器语言
* @param systemKey 系统关键字
* @since 5.5.7
*/
public FtpConfig(String host, int port, String user, String password, Charset charset, String serverLanguageCode, String systemKey) {
this.host = host;
this.port = port;
this.user = user;
this.password = password;
this.charset = charset;
this.serverLanguageCode = serverLanguageCode;
this.systemKey = systemKey;
}
public String getHost() {
@ -131,4 +159,22 @@ public class FtpConfig implements Serializable {
this.soTimeout = soTimeout;
return this;
}
public String getServerLanguageCode() {
return serverLanguageCode;
}
public FtpConfig setServerLanguageCode(String serverLanguageCode) {
this.serverLanguageCode = serverLanguageCode;
return this;
}
public String getSystemKey() {
return systemKey;
}
public FtpConfig setSystemKey(String systemKey) {
this.systemKey = systemKey;
return this;
}
}

View File

@ -12,7 +12,6 @@ import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.SendFailedException;
@ -25,6 +24,7 @@ import javax.mail.util.ByteArrayDataSource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.Date;
@ -77,11 +77,16 @@ public class Mail {
*/
private boolean useGlobalSession = false;
/**
* debug输出位置可以自定义debug日志
*/
private PrintStream debugOutput;
/**
* 创建邮件客户端
*
* @param mailAccount 邮件帐号
* @return {@link Mail}
* @return Mail
*/
public static Mail create(MailAccount mailAccount) {
return new Mail(mailAccount);
@ -90,7 +95,7 @@ public class Mail {
/**
* 创建邮件客户端使用全局邮件帐户
*
* @return {@link Mail}
* @return Mail
*/
public static Mail create() {
return new Mail();
@ -345,6 +350,18 @@ public class Mail {
this.useGlobalSession = isUseGlobalSession;
return this;
}
/**
* 设置debug输出位置可以自定义debug日志
*
* @param debugOutput debug输出位置
* @return this
* @since 5.5.6
*/
public Mail setDebugOutput(PrintStream debugOutput) {
this.debugOutput = debugOutput;
return this;
}
// --------------------------------------------------------------- Getters and Setters end
/**
@ -389,7 +406,7 @@ public class Mail {
*/
private MimeMessage buildMsg() throws MessagingException {
final Charset charset = this.mailAccount.getCharset();
final MimeMessage msg = new MimeMessage(getSession(this.useGlobalSession));
final MimeMessage msg = new MimeMessage(getSession());
// 发件人
final String from = this.mailAccount.getFrom();
if (StrUtil.isEmpty(from)) {
@ -442,19 +459,16 @@ public class Mail {
* 获取默认邮件会话<br>
* 如果为全局单例的会话则全局只允许一个邮件帐号否则每次发送邮件会新建一个新的会话
*
* @param isSingleton 是否使用单例Session
* @return 邮件会话 {@link Session}
* @since 4.0.2
*/
private Session getSession(boolean isSingleton) {
final MailAccount mailAccount = this.mailAccount;
Authenticator authenticator = null;
if (mailAccount.isAuth()) {
authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
private Session getSession() {
final Session session = MailUtil.getSession(this.mailAccount, this.useGlobalSession);
if(null != this.debugOutput){
session.setDebugOut(debugOutput);
}
return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
: Session.getInstance(mailAccount.getSmtpProps(), authenticator);
return session;
}
// --------------------------------------------------------------- Private method end
}

View File

@ -23,8 +23,10 @@ public class MailAccount implements Serializable {
private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
// SSL
private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";
@ -80,6 +82,12 @@ public class MailAccount implements Serializable {
* 使用 SSL安全连接
*/
private Boolean sslEnable;
/**
* SSL协议多个协议用空格分隔
*/
private String sslProtocols;
/**
* 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*/
@ -356,6 +364,25 @@ public class MailAccount implements Serializable {
return this;
}
/**
* 获取SSL协议多个协议用空格分隔
* @return SSL协议多个协议用空格分隔
* @since 5.5.7
*/
public String getSslProtocols() {
return sslProtocols;
}
/**
* 设置SSL协议多个协议用空格分隔
*
* @param sslProtocols SSL协议多个协议用空格分隔
* @since 5.5.7
*/
public void setSslProtocols(String sslProtocols) {
this.sslProtocols = sslProtocols;
}
/**
* 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*
@ -479,6 +506,10 @@ public class MailAccount implements Serializable {
p.put(SOCKET_FACTORY, socketFactoryClass);
p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));
p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));
// issue#IZN95@Gitee在Linux下需自定义SSL协议版本
if(StrUtil.isNotBlank(this.sslProtocols)){
p.put(SSL_PROTOCOLS, this.sslProtocols);
}
}
return p;

View File

@ -5,6 +5,8 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import javax.mail.Authenticator;
import javax.mail.Session;
import java.io.File;
import java.io.InputStream;
import java.util.Collection;
@ -346,6 +348,24 @@ public class MailUtil {
return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
}
/**
* 根据配置文件获取邮件客户端会话
*
* @param mailAccount 邮件账户配置
* @param isSingleton 是否单例全局共享会话
* @return {@link Session}
* @since 5.5.7
*/
public static Session getSession(MailAccount mailAccount, boolean isSingleton){
Authenticator authenticator = null;
if (mailAccount.isAuth()) {
authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
}
return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
: Session.getInstance(mailAccount.getSmtpProps(), authenticator);
}
// ------------------------------------------------------------------------------------------------------------------------ Private method start
/**

View File

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

View File

@ -1064,10 +1064,10 @@ public class HttpRequest extends HttpBase<HttpRequest> {
this.httpConnection = HttpConnection
.create(this.url.toURL(this.urlHandler), this.proxy)//
.setMethod(this.method)//
.setHttpsInfo(this.hostnameVerifier, this.ssf)//
.setConnectTimeout(this.connectionTimeout)//
.setReadTimeout(this.readTimeout)//
.setMethod(this.method)//
.setHttpsInfo(this.hostnameVerifier, this.ssf)//
// 定义转发
.setInstanceFollowRedirects(this.maxRedirectCount > 0)
// 流方式上传数据

View File

@ -1,6 +1,7 @@
package cn.hutool.http.server;
import cn.hutool.core.util.CharsetUtil;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import java.nio.charset.Charset;
@ -34,4 +35,14 @@ public class HttpServerBase {
public HttpExchange getHttpExchange() {
return this.httpExchange;
}
/**
* 获取{@link HttpContext}
*
* @return {@link HttpContext}
* @since 5.5.7
*/
public HttpContext getHttpContext() {
return getHttpExchange().getHttpContext();
}
}

View File

@ -46,7 +46,7 @@ public class HttpServerResponse extends HttpServerBase {
}
/**
* 发送HTTP状态码
* 发送HTTP状态码Content-Length为0不定长度会输出Transfer-encoding: chunked
*
* @param httpStatusCode HTTP状态码见HttpStatus
* @return this
@ -64,6 +64,17 @@ public class HttpServerResponse extends HttpServerBase {
return send(HttpStatus.HTTP_OK);
}
/**
* 发送成功状态码
*
* @param bodyLength 响应体长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
* @since 5.5.7
*/
public HttpServerResponse sendOk(int bodyLength) {
return send(HttpStatus.HTTP_OK, bodyLength);
}
/**
* 发送404错误页
*
@ -91,7 +102,7 @@ public class HttpServerResponse extends HttpServerBase {
* 发送HTTP状态码
*
* @param httpStatusCode HTTP状态码见HttpStatus
* @param bodyLength 响应体长度默认0
* @param bodyLength 响应体长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
public HttpServerResponse send(int httpStatusCode, long bodyLength) {
@ -290,7 +301,8 @@ public class HttpServerResponse extends HttpServerBase {
* @return this
*/
public HttpServerResponse write(byte[] data) {
return write(new ByteArrayInputStream(data));
final ByteArrayInputStream in = new ByteArrayInputStream(data);
return write(in, in.available());
}
/**
@ -302,8 +314,21 @@ public class HttpServerResponse extends HttpServerBase {
* @since 5.2.6
*/
public HttpServerResponse write(InputStream in, String contentType) {
return write(in, 0, contentType);
}
/**
* 返回数据给客户端
*
* @param in 需要返回客户端的内容
* @param length 内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @param contentType 返回的类型
* @return this
* @since 5.2.7
*/
public HttpServerResponse write(InputStream in, int length, String contentType) {
setContentType(contentType);
return write(in);
return write(in, length);
}
/**
@ -313,9 +338,23 @@ public class HttpServerResponse extends HttpServerBase {
* @return this
*/
public HttpServerResponse write(InputStream in) {
return write(in, 0);
}
/**
* 写出数据到客户端
*
* @param in 数据流
* @param length 指定响应内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
public HttpServerResponse write(InputStream in, int length) {
if (false == isSendCode) {
sendOk(Math.max(0, length));
}
OutputStream out = null;
try {
out = getOut();
out = this.httpExchange.getResponseBody();
IoUtil.copy(in, out);
} finally {
IoUtil.close(out);
@ -332,12 +371,16 @@ public class HttpServerResponse extends HttpServerBase {
* @since 5.2.6
*/
public HttpServerResponse write(File file) {
final long fileSize = file.length();
if(fileSize > Integer.MAX_VALUE){
throw new IllegalArgumentException("File size is too bigger than " + Integer.MAX_VALUE);
}
final String fileName = file.getName();
final String contentType = ObjectUtil.defaultIfNull(HttpUtil.getMimeType(fileName), "application/octet-stream");
BufferedInputStream in = null;
try {
in = FileUtil.getInputStream(file);
write(in, contentType, fileName);
write(in, (int)fileSize, contentType, fileName);
} finally {
IoUtil.close(in);
}
@ -353,12 +396,24 @@ public class HttpServerResponse extends HttpServerBase {
* @since 5.2.6
*/
public void write(InputStream in, String contentType, String fileName) {
write(in, 0, contentType, fileName);
}
/**
* 返回文件数据给客户端文件下载
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @param fileName 文件名
* @since 5.2.7
*/
public HttpServerResponse write(InputStream in, int length, String contentType, String fileName) {
final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
if(false == contentType.startsWith("text/")){
if (false == contentType.startsWith("text/")) {
// 非文本类型数据直接走下载
setHeader(Header.CONTENT_DISPOSITION, StrUtil.format("attachment;filename={}", URLUtil.encode(fileName, charset)));
}
write(in, contentType);
return write(in, length, contentType);
}
}

View File

@ -2,15 +2,26 @@ package cn.hutool.http.server;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.lang.Console;
import cn.hutool.core.thread.GlobalThreadPool;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.action.Action;
import cn.hutool.http.server.action.RootAction;
import cn.hutool.http.server.filter.HttpFilter;
import cn.hutool.http.server.filter.SimpleFilter;
import cn.hutool.http.server.handler.ActionHandler;
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
@ -21,7 +32,8 @@ import java.util.concurrent.Executor;
*/
public class SimpleServer {
HttpServer server;
private final HttpServer server;
private final List<Filter> filters;
/**
* 构造
@ -48,25 +60,107 @@ public class SimpleServer {
* @param address 监听地址
*/
public SimpleServer(InetSocketAddress address) {
this(address, null);
}
/**
* 构造
*
* @param address 监听地址
* @param configurator https配置信息用于使用自定义SSLTLS证书等
*/
public SimpleServer(InetSocketAddress address, HttpsConfigurator configurator) {
try {
if(null != configurator){
final HttpsServer server = HttpsServer.create(address, 0);
server.setHttpsConfigurator(configurator);
this.server = server;
} else{
this.server = HttpServer.create(address, 0);
}
} catch (IOException e) {
throw new IORuntimeException(e);
}
setExecutor(GlobalThreadPool.getExecutor());
filters = new ArrayList<>();
}
/**
* 增加请求过滤器此过滤器对所有请求有效<br>
* 此方法需在以下方法前之前调用
*
* <ul>
* <li>{@link #setRoot(File)} </li>
* <li>{@link #setRoot(String)} </li>
* <li>{@link #createContext(String, HttpHandler)} </li>
* <li>{@link #addHandler(String, HttpHandler)}</li>
* <li>{@link #addAction(String, Action)} </li>
* </ul>
*
* @param filter {@link Filter} 请求过滤器
* @return this
* @since 5.5.7
*/
public SimpleServer addFilter(Filter filter) {
this.filters.add(filter);
return this;
}
/**
* 增加请求过滤器此过滤器对所有请求有效<br>
* 此方法需在以下方法前之前调用
*
* <ul>
* <li>{@link #setRoot(File)} </li>
* <li>{@link #setRoot(String)} </li>
* <li>{@link #createContext(String, HttpHandler)} </li>
* <li>{@link #addHandler(String, HttpHandler)}</li>
* <li>{@link #addAction(String, Action)} </li>
* </ul>
*
* @param filter {@link Filter} 请求过滤器
* @return this
* @since 5.5.7
*/
public SimpleServer addFilter(HttpFilter filter) {
return addFilter(new SimpleFilter() {
@Override
public void doFilter(HttpExchange httpExchange, Chain chain) throws IOException {
filter.doFilter(new HttpServerRequest(httpExchange), new HttpServerResponse(httpExchange), chain);
}
});
}
/**
* 增加请求处理规则
*
* @param path 路径
* @param handler 处理器
* @param path 路径例如:/a/b 或者 a/b
* @param handler 处理器包括请求和响应处理
* @return this
* @see #createContext(String, HttpHandler)
*/
public SimpleServer addHandler(String path, HttpHandler handler) {
this.server.createContext(path, handler);
createContext(path, handler);
return this;
}
/**
* 创建请求映射上下文创建后用户访问指定路径可使用{@link HttpHandler} 中的规则进行处理
*
* @param path 路径例如:/a/b 或者 a/b
* @param handler 处理器包括请求和响应处理
* @return {@link HttpContext}
* @since 5.5.7
*/
public HttpContext createContext(String path, HttpHandler handler) {
// /开头的路径会报错
path = StrUtil.addPrefixIfNot(path, StrUtil.SLASH);
final HttpContext context = this.server.createContext(path, handler);
// 增加整体过滤器
context.getFilters().addAll(this.filters);
return context;
}
/**
* 设置根目录默认的页面从root目录中读取解析返回
*

View File

@ -2,7 +2,6 @@ package cn.hutool.http.server.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
@ -80,7 +79,6 @@ public class RootAction implements Action {
}
}
Console.log(file.getAbsolutePath());
response.send404("404 Not Found !");
}
}

View File

@ -0,0 +1,6 @@
/**
* {@link com.sun.net.httpserver.HttpServer} 封装
*
* @author looly
*/
package cn.hutool.http.server.action;

View File

@ -0,0 +1,26 @@
package cn.hutool.http.server.filter;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import com.sun.net.httpserver.Filter;
import java.io.IOException;
/**
* 过滤器接口用于简化{@link Filter} 使用
*
* @author looly
* @since 5.5.7
*/
@FunctionalInterface
public interface HttpFilter {
/**
* 执行过滤
* @param req {@link HttpServerRequest} 请求对象用于获取请求内容
* @param res {@link HttpServerResponse} 响应对象用于写出内容
* @param chain {@link Filter.Chain}
* @throws IOException IO异常
*/
void doFilter(HttpServerRequest req, HttpServerResponse res, Filter.Chain chain) throws IOException;
}

View File

@ -0,0 +1,17 @@
package cn.hutool.http.server.filter;
import com.sun.net.httpserver.Filter;
/**
* 匿名简单过滤器跳过了描述
*
* @author looly
* @since 5.5.7
*/
public abstract class SimpleFilter extends Filter {
@Override
public String description() {
return "Anonymous Filter";
}
}

View File

@ -0,0 +1,4 @@
/**
* {@link com.sun.net.httpserver.Filter} 实现包装
*/
package cn.hutool.http.server.filter;

View File

@ -0,0 +1,4 @@
/**
* {@link com.sun.net.httpserver.HttpHandler} 实现包装
*/
package cn.hutool.http.server.handler;

View File

@ -1,19 +1,20 @@
package cn.hutool.http.ssl;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.SSLv3;
import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.TLSv1;
import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.TLSv11;
import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.TLSv12;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
/**
* 兼容android低版本SSL连接
* 咱在测试HttpUrlConnection的时候
* 发现一部分手机无法连接[GithubPage]
* 兼容android低版本SSL连接<br>
* 在测试HttpUrlConnection的时候发现一部分手机无法连接[GithubPage]
*
* <p>
* 最后发现原来是某些SSL协议没有开启
*
* @author MikaGuraNTK
*/
public class AndroidSupportSSLFactory extends CustomProtocolsSSLFactory {

View File

@ -25,7 +25,7 @@ public class HttpRequestTest {
@Test
@Ignore
public void getHttpsTest() {
String body = HttpRequest.get("https://www.gjifa.com/pc/").execute().body();
String body = HttpRequest.get("https://www.hutool.cn/").timeout(10).execute().body();
Console.log(body);
}

View File

@ -4,9 +4,6 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

View File

@ -11,6 +11,10 @@ public class SimpleServerTest {
public static void main(String[] args) {
HttpUtil.createServer(8888)
.addFilter(((req, res, chain) -> {
Console.log("Filter: " + req.getPath());
chain.doFilter(req.getHttpExchange());
}))
// 设置默认根目录classpath/html
.setRoot(FileUtil.file("html"))
// get数据测试返回请求的PATH
@ -45,6 +49,11 @@ public class SimpleServerTest {
response.write(request.getMultipart().getParamMap().toString(), ContentType.TEXT_PLAIN.toString());
}
)
// 测试输出响应内容是否能正常返回Content-Length头信息
.addAction("test/zeroStr", (req, res)-> {
res.write("0");
Console.log("Write 0 OK");
})
.start();
}
}

View File

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

View File

@ -253,7 +253,7 @@ final class InternalJSONUtil {
//默认使用时间戳
long timeMillis;
if (dateObj instanceof TemporalAccessor) {
timeMillis = DateUtil.toInstant((TemporalAccessor) dateObj).toEpochMilli();
timeMillis = TemporalAccessorUtil.toEpochMilli((TemporalAccessor) dateObj);
} else if (dateObj instanceof Date) {
timeMillis = ((Date) dateObj).getTime();
} else if (dateObj instanceof Calendar) {

View File

@ -1,8 +1,8 @@
package cn.hutool.json;
/**
* <code>JSONString</code>接口定义了一个<code>toJSONString()</code><br>
* 实现此接口的类可以通过实现<code>toJSONString()</code>方法来改变转JSON字符串的方式
* {@code JSONString}接口定义了一个{@code toJSONString()}<br>
* 实现此接口的类可以通过实现{@code toJSONString()}方法来改变转JSON字符串的方式
*
* @author Looly
*

View File

@ -35,7 +35,7 @@ public class JSONSupport implements JSONString{
* @return 美化的JSON
*/
public String toPrettyString() {
return toJSON().toJSONString(4);
return toJSON().toStringPretty();
}
@Override

View File

@ -218,7 +218,7 @@ public final class JSONUtil {
json = (JSON) obj;
} else if (obj instanceof CharSequence) {
final String jsonStr = StrUtil.trim((CharSequence) obj);
json = StrUtil.startWith(jsonStr, '[') ? parseArray(jsonStr) : parseObj(jsonStr);
json = isJsonArray(jsonStr) ? parseArray(jsonStr, config) : parseObj(jsonStr, config);
} else if (obj instanceof Iterable || obj instanceof Iterator || ArrayUtil.isArray(obj)) {// 列表
json = new JSONArray(obj, config);
} else {// 对象

View File

@ -11,6 +11,7 @@ import cn.hutool.core.util.CharsetUtil;
import cn.hutool.json.test.bean.Exam;
import cn.hutool.json.test.bean.JsonNode;
import cn.hutool.json.test.bean.KeyBean;
import lombok.Data;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
@ -202,6 +203,14 @@ public class JSONArrayTest {
Assert.assertEquals("-0", nodeList.get(3).getName());
}
@Test
public void getByPathTest(){
String jsonStr = "[{\"id\": \"1\",\"name\": \"a\"},{\"id\": \"2\",\"name\": \"b\"}]";
final JSONArray jsonArray = JSONUtil.parseArray(jsonStr);
Assert.assertEquals("b", jsonArray.getByPath("[1].name"));
Assert.assertEquals("b", JSONUtil.getByPath(jsonArray, "[1].name"));
}
private static Map<String, String> buildMap(String id, String parentId, String name) {
Map<String, String> map = new HashMap<>();
map.put("id", id);
@ -210,25 +219,9 @@ public class JSONArrayTest {
return map;
}
@Data
static class User {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
}

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.test.bean.Price;
import cn.hutool.json.test.bean.UserA;
import cn.hutool.json.test.bean.UserC;
@ -163,6 +164,14 @@ public class JSONUtilTest {
Assert.assertEquals("12.00", jsonObject.getBigDecimal("test").setScale(2).toString());
}
@Test
public void customValueTest() {
final JSONObject jsonObject = JSONUtil.createObj()
.set("test2", (JSONString) () -> NumberUtil.decimalFormat("#.0", 12.00D));
Assert.assertEquals("{\"test2\":12.0}", jsonObject.toString());
}
@Test
public void parseObjTest() {
// 测试转义
@ -171,4 +180,3 @@ public class JSONUtilTest {
"}");
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-log</artifactId>
@ -92,7 +92,7 @@
<dependency>
<groupId>org.tinylog</groupId>
<artifactId>tinylog-impl</artifactId>
<version>2.1.2</version>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -8,7 +8,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-poi</artifactId>

View File

@ -145,7 +145,7 @@ public class ExcelBase<T extends ExcelBase<T>> implements Closeable {
}
/**
* 获取指定坐标单元格单元格不存在时返回<code>null</code>
* 获取指定坐标单元格单元格不存在时返回{@code null}
*
* @param locationRef 单元格地址标识符例如A11B5
* @return {@link Cell}
@ -157,7 +157,7 @@ public class ExcelBase<T extends ExcelBase<T>> implements Closeable {
}
/**
* 获取指定坐标单元格单元格不存在时返回<code>null</code>
* 获取指定坐标单元格单元格不存在时返回{@code null}
*
* @param x X坐标从0计数即列号
* @param y Y坐标从0计数即行号
@ -193,7 +193,7 @@ public class ExcelBase<T extends ExcelBase<T>> implements Closeable {
}
/**
* 获取指定坐标单元格如果isCreateIfNotExist为false则在单元格不存在时返回<code>null</code>
* 获取指定坐标单元格如果isCreateIfNotExist为false则在单元格不存在时返回{@code null}
*
* @param locationRef 单元格地址标识符例如A11B5
* @param isCreateIfNotExist 单元格不存在时是否创建
@ -206,7 +206,7 @@ public class ExcelBase<T extends ExcelBase<T>> implements Closeable {
}
/**
* 获取指定坐标单元格如果isCreateIfNotExist为false则在单元格不存在时返回<code>null</code>
* 获取指定坐标单元格如果isCreateIfNotExist为false则在单元格不存在时返回{@code null}
*
* @param x X坐标从0计数即列号
* @param y Y坐标从0计数即行号

View File

@ -233,7 +233,7 @@ public class ExcelWriter extends ExcelBase<ExcelWriter> {
/**
* 重命名sheet
*
* @param sheet sheet需要0表示第一个sheet
* @param sheet sheet序号0表示第一个sheet
* @param sheetName 新的sheet名
* @return this
* @since 4.1.8

View File

@ -1,6 +1,7 @@
package cn.hutool.poi.excel.sax;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.sax.handler.RowHandler;
@ -116,12 +117,12 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
* 读取
*
* @param fs {@link POIFSFileSystem}
* @param id sheet序号
* @param id sheet序号从0开始
* @return this
* @throws POIException IO异常包装
*/
public Excel03SaxReader read(POIFSFileSystem fs, String id) throws POIException {
this.rid = Integer.parseInt(id);
this.rid = getSheetIndex(id);
formatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this));
final HSSFRequest request = new HSSFRequest();
@ -341,5 +342,32 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
private boolean isProcessCurrentSheet() {
return this.rid < 0 || this.curRid == this.rid;
}
/**
* 获取sheet索引从0开始
* <ul>
* <li>传入'rId'开头直接去除rId前缀</li>
* <li>传入纯数字表示sheetIndex直接转换为rid</li>
* </ul>
*
* @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称从0开始rid必须加rId前缀例如rId0如果为-1处理所有编号的sheet
* @return sheet索引从0开始
* @since 5.5.5
*/
private int getSheetIndex(String idOrRidOrSheetName) {
Assert.notBlank(idOrRidOrSheetName, "id or rid or sheetName must be not blank!");
// rid直接处理
if (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) {
return Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX));
}
final int sheetIndex;
try {
return Integer.parseInt(idOrRidOrSheetName);
} catch (NumberFormatException ignore) {
throw new IllegalArgumentException("Invalid sheet id: " + idOrRidOrSheetName);
}
}
// ---------------------------------------------------------------------------------------------- Private method end
}

View File

@ -2,7 +2,6 @@ package cn.hutool.poi.excel.sax;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.sax.handler.RowHandler;
import cn.hutool.poi.exceptions.POIException;
@ -25,8 +24,6 @@ import java.util.Iterator;
*/
public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
// sheet r:Id前缀
private static final String RID_PREFIX = "rId";
private final SheetDataSaxHandler handler;
/**
@ -56,9 +53,9 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
}
@Override
public Excel07SaxReader read(File file, String idOrRid) throws POIException {
public Excel07SaxReader read(File file, String idOrRidOrSheetName) throws POIException {
try {
return read(OPCPackage.open(file), idOrRid);
return read(OPCPackage.open(file), idOrRidOrSheetName);
} catch (InvalidFormatException e) {
throw new POIException(e);
}
@ -70,9 +67,9 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
}
@Override
public Excel07SaxReader read(InputStream in, String idOrRid) throws POIException {
public Excel07SaxReader read(InputStream in, String idOrRidOrSheetName) throws POIException {
try (final OPCPackage opcPackage = OPCPackage.open(in)) {
return read(opcPackage, idOrRid);
return read(opcPackage, idOrRidOrSheetName);
} catch (IOException e) {
throw new IORuntimeException(e);
} catch (InvalidFormatException e) {
@ -96,13 +93,13 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
* 开始读取ExcelSheet编号从0开始计数
*
* @param opcPackage {@link OPCPackage}Excel包读取后不关闭
* @param idOrRid Excel中的sheet id或者rid编号rid必须加rId前缀例如rId1如果为-1处理所有编号的sheet
* @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名rid必须加rId前缀例如rId1如果为-1处理所有编号的sheet
* @return this
* @throws POIException POI异常
*/
public Excel07SaxReader read(OPCPackage opcPackage, String idOrRid) throws POIException {
public Excel07SaxReader read(OPCPackage opcPackage, String idOrRidOrSheetName) throws POIException {
try {
return read(new XSSFReader(opcPackage), idOrRid);
return read(new XSSFReader(opcPackage), idOrRidOrSheetName);
} catch (OpenXML4JException e) {
throw new POIException(e);
} catch (IOException e) {
@ -114,12 +111,12 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
* 开始读取ExcelSheet编号从0开始计数
*
* @param xssfReader {@link XSSFReader}Excel读取器
* @param idOrRid Excel中的sheet id或者rid编号rid必须加rId前缀例如rId1如果为-1处理所有编号的sheet
* @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名rid必须加rId前缀例如rId1如果为-1处理所有编号的sheet
* @return this
* @throws POIException POI异常
* @since 5.4.4
*/
public Excel07SaxReader read(XSSFReader xssfReader, String idOrRid) throws POIException {
public Excel07SaxReader read(XSSFReader xssfReader, String idOrRidOrSheetName) throws POIException {
// 获取共享样式表样式非必须
try {
this.handler.stylesTable = xssfReader.getStylesTable();
@ -136,7 +133,7 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
throw new POIException(e);
}
return readSheets(xssfReader, idOrRid);
return readSheets(xssfReader, idOrRidOrSheetName);
}
// ------------------------------------------------------------------------------ Read end
@ -146,21 +143,13 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
* 开始读取ExcelSheet编号从0开始计数
*
* @param xssfReader {@link XSSFReader}Excel读取器
* @param idOrRid Excel中的sheet id或者rid编号rid必须加rId前缀例如rId0如果为-1处理所有编号的sheet
* @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名从0开始rid必须加rId前缀例如rId0如果为-1处理所有编号的sheet
* @return this
* @throws POIException POI异常
* @since 5.4.4
*/
private Excel07SaxReader readSheets(XSSFReader xssfReader, String idOrRid) throws POIException {
// 将sheetId转换为rid
if (NumberUtil.isInteger(idOrRid)) {
final SheetRidReader ridReader = new SheetRidReader();
final String rid = ridReader.read(xssfReader).getRidBySheetId(idOrRid);
if (StrUtil.isNotEmpty(rid)) {
idOrRid = rid;
}
}
this.handler.sheetIndex = Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRid, RID_PREFIX));
private Excel07SaxReader readSheets(XSSFReader xssfReader, String idOrRidOrSheetName) throws POIException {
this.handler.sheetIndex = getSheetIndex(xssfReader, idOrRidOrSheetName);
InputStream sheetInputStream = null;
try {
if (this.handler.sheetIndex > -1) {
@ -190,5 +179,44 @@ public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
}
return this;
}
/**
* 获取sheet索引从0开始
* <ul>
* <li>传入'rId'开头直接去除rId前缀</li>
* <li>传入纯数字表示sheetIndex通过{@link SheetRidReader}转换为rId</li>
* <li>传入其它字符串表示sheetName通过{@link SheetRidReader}转换为rId</li>
* </ul>
*
* @param xssfReader {@link XSSFReader}Excel读取器
* @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称从0开始rid必须加rId前缀例如rId0如果为-1处理所有编号的sheet
* @return sheet索引从0开始
* @since 5.5.5
*/
private int getSheetIndex(XSSFReader xssfReader, String idOrRidOrSheetName) {
// rid直接处理
if (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) {
return Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX));
}
// sheetIndex需转换为rid
final SheetRidReader ridReader = new SheetRidReader().read(xssfReader);
final int sheetIndex;
Integer rid;
try {
sheetIndex = Integer.parseInt(idOrRidOrSheetName);
rid = ridReader.getRidBySheetIdBase0(sheetIndex);
return (null != rid) ? rid : sheetIndex;
} catch (NumberFormatException ignore) {
// 非数字可能为sheet名称
rid = ridReader.getRidByNameBase0(idOrRidOrSheetName);
if (null != rid) {
return rid;
}
}
throw new IllegalArgumentException("Invalid rId or id or sheetName: " + idOrRidOrSheetName);
}
// --------------------------------------------------------------------------------------- Private method end
}

View File

@ -15,6 +15,9 @@ import java.io.InputStream;
*/
public interface ExcelSaxReader<T> {
// sheet r:Id前缀
String RID_PREFIX = "rId";
/**
* 开始读取Excel
*

View File

@ -29,7 +29,7 @@ public class SheetDataSaxHandler extends DefaultHandler {
protected StylesTable stylesTable;
// excel 2007 的共享字符串表,对应sharedString.xml
protected SharedStringsTable sharedStringsTable;
// sheet的索引
// sheet的索引从0开始
protected int sheetIndex;
// 当前非空行

View File

@ -1,5 +1,6 @@
package cn.hutool.poi.excel.sax;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
@ -11,13 +12,13 @@ import org.xml.sax.helpers.DefaultHandler;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 在Sax方式读取Excel时读取sheet标签中sheetId和rid的对应关系类似于:
* <pre>
* &lt;sheet name="Sheet6" sheetId="4" r:id="6"/&gt;
* &lt;sheet name="Sheet6" sheetId="4" r:id="rId6"/&gt;
* </pre>
* <p>
* 读取结果为
@ -36,8 +37,8 @@ public class SheetRidReader extends DefaultHandler {
private final static String SHEET_ID_ATTR = "sheetId";
private final static String NAME_ATTR = "name";
private final Map<String, String> ID_RID_MAP = new HashMap<>();
private final Map<String, String> NAME_RID_MAP = new HashMap<>();
private final Map<Integer, Integer> ID_RID_MAP = new LinkedHashMap<>();
private final Map<String, Integer> NAME_RID_MAP = new LinkedHashMap<>();
/**
* 读取Wordkbook的XML中sheet标签中sheetId和rid的对应关系
@ -61,50 +62,100 @@ public class SheetRidReader extends DefaultHandler {
}
/**
* 根据sheetId获取rid
* 根据sheetId获取rid从1开始
*
* @param sheetId Sheet的ID
* @return rid
* @param sheetId Sheet的ID从1开始
* @return rid从1开始
*/
public String getRidBySheetId(String sheetId) {
public Integer getRidBySheetId(int sheetId) {
return ID_RID_MAP.get(sheetId);
}
/**
* 根据sheet name获取rid
* 根据sheetId获取rid从0开始
*
* @param sheetId Sheet的ID从0开始
* @return rid从0开始
* @since 5.5.5
*/
public Integer getRidBySheetIdBase0(int sheetId) {
final Integer rid = getRidBySheetId(sheetId + 1);
if (null != rid) {
return rid - 1;
}
return null;
}
/**
* 根据sheet name获取rid从1开始
*
* @param sheetName Sheet的name
* @return rid
* @return rid从1开始
*/
public String getRidByName(String sheetName) {
public Integer getRidByName(String sheetName) {
return NAME_RID_MAP.get(sheetName);
}
/**
* 根据sheet name获取rid从0开始
*
* @param sheetName Sheet的name
* @return rid从0开始
* @since 5.5.5
*/
public Integer getRidByNameBase0(String sheetName) {
final Integer rid = getRidByName(sheetName);
if (null != rid) {
return rid - 1;
}
return null;
}
/**
* 通过sheet的序号获取rid
*
* @param index 序号从0开始
* @return rid
* @since 5.5.7
*/
public Integer getRidByIndex(int index) {
return CollUtil.get(this.NAME_RID_MAP.values(), index);
}
/**
* 通过sheet的序号获取rid
*
* @param index 序号从0开始
* @return rid从0开始
* @since 5.5.7
*/
public Integer getRidByIndexBase0(int index) {
final Integer rid = CollUtil.get(this.NAME_RID_MAP.values(), index);
if (null != rid) {
return rid - 1;
}
return null;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if (TAG_NAME.equalsIgnoreCase(localName)) {
final int length = attributes.getLength();
String sheetId = null;
String rid = null;
String name = null;
for (int i = 0; i < length; i++) {
switch (attributes.getLocalName(i)) {
case SHEET_ID_ATTR:
sheetId = attributes.getValue(i);
break;
case RID_ATTR:
rid = attributes.getValue(i);
break;
case NAME_ATTR:
name = attributes.getValue(i);
break;
}
if (StrUtil.isNotEmpty(sheetId)) {
ID_RID_MAP.put(sheetId, rid);
final String ridStr = attributes.getValue(RID_ATTR);
if (StrUtil.isEmpty(ridStr)) {
return;
}
final int rid = Integer.parseInt(StrUtil.removePrefixIgnoreCase(ridStr, Excel07SaxReader.RID_PREFIX));
// sheet名和rid映射
final String name = attributes.getValue(NAME_ATTR);
if (StrUtil.isNotEmpty(name)) {
NAME_RID_MAP.put(name, rid);
}
// sheetId和rid映射
final String sheetIdStr = attributes.getValue(SHEET_ID_ATTR);
if (StrUtil.isNotEmpty(sheetIdStr)) {
ID_RID_MAP.put(Integer.parseInt(sheetIdStr), rid);
}
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.map.MapUtil;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFTable;
@ -33,7 +34,7 @@ public class TableUtil {
}
/**
* 创建表格并填充数据
* 创建表格并填充数据默认表格
*
* @param doc {@link XWPFDocument}
* @param data 数据
@ -41,17 +42,33 @@ public class TableUtil {
*/
public static XWPFTable createTable(XWPFDocument doc, Iterable<?> data) {
Assert.notNull(doc, "XWPFDocument must be not null !");
XWPFTable table = doc.createTable();
final XWPFTable table = doc.createTable();
// 新建table的时候默认会新建一行此处移除之
table.removeRow(0);
return writeTable(table, data);
}
/**
* 为table填充数据
*
* @param table {@link XWPFTable}
* @param data 数据
* @return {@link XWPFTable}
* @since 5.5.6
*/
public static XWPFTable writeTable(XWPFTable table, Iterable<?> data){
Assert.notNull(table, "XWPFTable must be not null !");
if (IterUtil.isEmpty(data)) {
// 数据为空返回空表
return table;
}
int index = 0;
boolean isFirst = true;
for (Object rowData : data) {
writeRow(getOrCreateRow(table, index), rowData, true);
index ++;
writeRow(table.createRow(), rowData, isFirst);
if(isFirst){
isFirst = false;
}
}
return table;

View File

@ -58,9 +58,19 @@ public class ExcelSaxReadTest {
@Test
public void readBySaxTest() {
ExcelUtil.readBySax("blankAndDateTest.xlsx", "0", createRowHandler());
}
@Test
public void readBySaxByRidTest() {
ExcelUtil.readBySax("blankAndDateTest.xlsx", 0, createRowHandler());
}
@Test
public void readBySaxByNameTest() {
ExcelUtil.readBySax("blankAndDateTest.xlsx", "Sheet1", createRowHandler());
}
@Test
@Ignore
public void readBySaxTest2() {

View File

@ -1,14 +1,15 @@
package cn.hutool.poi.word;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.poi.word.Word07Writer;
import org.junit.Ignore;
import org.junit.Test;
import java.awt.Font;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
@ -49,4 +50,36 @@ public class WordWriterTest {
writer.addTable(CollUtil.newArrayList(map));
writer.flush(FileUtil.file("d:/test/test.docx"));
}
@Test
@Ignore
public void writeMapAsTableTest() {
Word07Writer writer = new Word07Writer();
Map<String, Object> data = new LinkedHashMap<>();
data.put("姓名", "张三");
data.put("年龄", 23);
data.put("成绩", 80.5);
data.put("是否合格", true);
data.put("考试日期", DateUtil.date());
Map<String, Object> data2 = new LinkedHashMap<>();
data2.put("姓名", "李四");
data2.put("年龄", 4);
data2.put("成绩", 59);
data2.put("是否合格", false);
data2.put("考试日期", DateUtil.date());
ArrayList<Map<String, Object>> mapArrayList = CollUtil.newArrayList(data, data2);
// 添加段落标题
writer.addText(new Font("方正小标宋简体", Font.PLAIN, 22), "我是第一部分");
// 添加段落正文
writer.addText(new Font("宋体", Font.PLAIN, 13), "我是正文第一部分");
writer.addTable(mapArrayList);
// 写出到文件
writer.flush(FileUtil.file("d:/test/a.docx"));
// 关闭
writer.close();
}
}

View File

@ -8,7 +8,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-script</artifactId>
@ -18,7 +18,7 @@
<properties>
<jython.version>2.7.2</jython.version>
<luaj.version>3.0.1</luaj.version>
<groovy.version>3.0.6</groovy.version>
<groovy.version>3.0.7</groovy.version>
</properties>
<dependencies>

View File

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

View File

@ -519,11 +519,25 @@ public class Setting extends AbsSetting implements Map<String, String> {
* @param key
* @param value
* @return 此key之前存在的值如果没有返回null
* @deprecated 此方法与getXXX参数顺序不一致容易造成问题建议使用{@link #putByGroup(String, String, String)}
*/
@Deprecated
public String put(String group, String key, String value) {
return this.groupedMap.put(group, key, value);
}
/**
* 将键值对加入到对应分组中
*
* @param key
* @param group 分组
* @param value
* @return 此key之前存在的值如果没有返回null
*/
public String putByGroup(String key, String group, String value) {
return this.groupedMap.put(group, key, value);
}
/**
* 从指定分组中删除指定值
*
@ -622,12 +636,29 @@ public class Setting extends AbsSetting implements Map<String, String> {
* @param key
* @param value
* @return 此key之前存在的值如果没有返回null
* @deprecated 此方法与getXXX参数顺序不一致容易引起请使用{@link #setByGroup(String, String, String)}
*/
@Deprecated
public Setting set(String group, String key, String value) {
this.put(group, key, value);
return this;
}
/**
* 将键值对加入到对应分组中<br>
* 此方法用于与getXXX统一参数顺序
*
* @param key
* @param group 分组
* @param value
* @return 此key之前存在的值如果没有返回null
* @since 5.5.7
*/
public Setting setByGroup(String key, String group, String value) {
this.putByGroup(key, group, value);
return this;
}
// ------------------------------------------------- Override Map interface
@Override
public boolean isEmpty() {

View File

@ -40,9 +40,9 @@ public class PropsUtil {
/**
* 获取给定路径找到的第一个配置文件<br>
* * name可以为不包括扩展名的文件名默认.setting为结尾也可以是文件名全称
* * name可以为不包括扩展名的文件名默认.properties为结尾也可以是文件名全称
*
* @param names 文件名如果没有扩展名默认为.setting
* @param names 文件名如果没有扩展名默认为.properties
*
* @return 当前环境下配置文件
*/

View File

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

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
</parent>
<artifactId>hutool-system</artifactId>
@ -26,7 +26,7 @@
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>5.3.6</version>
<version>5.3.7</version>
<scope>provided</scope>
</dependency>
</dependencies>

View File

@ -8,7 +8,7 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.5.5-SNAPSHOT</version>
<version>5.5.8-SNAPSHOT</version>
<name>hutool</name>
<description>Hutool是一个小而全的Java工具类库通过静态方法封装降低相关API的学习成本提高工作效率使Java拥有函数式语言般的优雅让Java语言也可以“甜甜的”。</description>
<url>https://github.com/looly/hutool</url>