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

This commit is contained in:
lzpeng723 2020-12-11 23:14:36 +08:00
commit 94cda2591a
45 changed files with 5748 additions and 4814 deletions

View File

@ -3,17 +3,35 @@
-------------------------------------------------------------------------------------------------------------
# 5.5.3 (2020-12-06)
# 5.5.3 (2020-12-11)
### 新特性
* 【core 】 IdcardUtil增加行政区划83issue#1277@Github
* 【core 】 multipart中int改为long解决大文件上传越界问题issue#I27WZ3@Gitee
* 【core 】 ListUtil.page增加检查pr#224@Gitee
* 【db 】 Db增加使用sql的page方法issue#247@Gitee
* 【cache 】 CacheObj的isExpired()逻辑修改issue#1295@Github
* 【json 】 JSONStrFormater改为JSONStrFormatter
* 【dfa 】 增加FoundWordpr#1290@Github
* 【core 】 增加Segmentpr#1290@Github
* 【core 】 增加CharSequenceUtil
* 【poi 】 Excel07SaxReader拆分出SheetDataSaxHandler
* 【core 】 CollUtil.addAll增加判空pr#228@Gitee
* 【core 】 修正DateUtil.betweenXXX注释错误issue#I28XGW@Gitee
* 【core 】 增加NioUtil
* 【core 】 增加GanymedUtil
### Bug修复
* 【cache 】 修复Cache中get重复misCount计数问题issue#1281@Github
* 【poi 】 修复sax读取自定义格式单元格无法识别日期类型的问题issue#1283@Github
* 【core 】 修复CollUtil.get越界问题issue#1292@Github
* 【core 】 修复TemporalAccessorUtil无法格式化LocalDate带时间问题issue#1289@Github
* 【json 】 修复自定义日期格式的LocalDateTime没有包装引号问题issue#1289@Github
* 【cache 】 get中unlock改为unlockReadissue#1294@Github
* 【db 】 修复表名包含点导致的问题issue#1300@Github
* 【poi 】 修复xdr:row标签导致的问题issue#1297@Github
* 【core 】 修复FileUtil.loopFiles使用FileFilter无效问题issue#I28V48@Gitee
* 【extra 】 修复JschUtil.execByShell返回空的问题issue#1067@Github
-------------------------------------------------------------------------------------------------------------

View File

@ -174,7 +174,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
return co.get(isUpdateLastAccess);
}
} finally {
lock.unlock(stamp);
lock.unlockRead(stamp);
}
// 过期

View File

@ -44,9 +44,8 @@ public class CacheObj<K, V> implements Serializable{
*/
boolean isExpired() {
if(this.ttl > 0) {
final long expiredTime = this.lastAccess + this.ttl;
// expiredTime > 0 杜绝Long类型溢出变负数问题当当前时间超过过期时间表示过期
return expiredTime > 0 && expiredTime < System.currentTimeMillis();
// 此处不考虑时间回拨
return (System.currentTimeMillis() - this.lastAccess) > this.ttl;
}
return false;
}

View File

@ -5,6 +5,14 @@
<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>

View File

@ -1870,7 +1870,7 @@ public class CollUtil {
/**
* Iterator转换为Enumeration
* <p>
* Adapt the specified <code>Iterator</code> to the <code>Enumeration</code> interface.
* Adapt the specified {@link Iterator} to the {@link Enumeration} interface.
*
* @param <E> 集合元素类型
* @param iter {@link Iterator}
@ -1883,7 +1883,7 @@ public class CollUtil {
/**
* Enumeration转换为Iterator
* <p>
* Adapt the specified <code>Enumeration</code> to the <code>Iterator</code> interface
* Adapt the specified {@code Enumeration} to the {@code Iterator} interface
*
* @param <E> 集合元素类型
* @param e {@link Enumeration}
@ -2108,6 +2108,9 @@ public class CollUtil {
* @return 原集合
*/
public static <T> Collection<T> addAll(Collection<T> collection, Iterable<T> iterable) {
if (iterable == null) {
return collection;
}
return addAll(collection, iterable.iterator());
}
@ -2186,7 +2189,7 @@ public class CollUtil {
}
// 检查越界
if (index >= size) {
if (index >= size || index < 0) {
return null;
}

View File

@ -95,7 +95,7 @@ public class DateBetween implements Serializable{
/**
* 计算两个日期相差月数<br>
* 在非重置情况下如果起始日期的天于结束日期的天月数要少算1不足1个月
* 在非重置情况下如果起始日期的天于结束日期的天月数要少算1不足1个月
*
* @param isReset 是否重置时间为起始时间重置天时分秒
* @return 相差月数
@ -122,7 +122,7 @@ public class DateBetween implements Serializable{
/**
* 计算两个日期相差年数<br>
* 在非重置情况下如果起始日期的月于结束日期的月年数要少算1不足1年
* 在非重置情况下如果起始日期的月于结束日期的月年数要少算1不足1年
*
* @param isReset 是否重置时间为起始时间重置月天时分秒
* @return 相差年数

View File

@ -1339,8 +1339,8 @@ public class DateUtil extends CalendarUtil {
* <pre>
* 有时候我们计算相差天数的时候需要忽略时分秒
* 比如2016-02-01 23:59:59和2016-02-02 00:00:00相差一秒
* 如果isReset为<code>false</code>相差天数为0
* 如果isReset为<code>true</code>相差天数将被计算为1
* 如果isReset为{@code false}相差天数为0
* 如果isReset为{@code true}相差天数将被计算为1
* </pre>
*
* @param beginDate 起始日期
@ -1375,7 +1375,7 @@ public class DateUtil extends CalendarUtil {
/**
* 计算两个日期相差月数<br>
* 在非重置情况下如果起始日期的天于结束日期的天月数要少算1不足1个月
* 在非重置情况下如果起始日期的天于结束日期的天月数要少算1不足1个月
*
* @param beginDate 起始日期
* @param endDate 结束日期
@ -1389,7 +1389,7 @@ public class DateUtil extends CalendarUtil {
/**
* 计算两个日期相差年数<br>
* 在非重置情况下如果起始日期的月于结束日期的月年数要少算1不足1年
* 在非重置情况下如果起始日期的月于结束日期的月年数要少算1不足1年
*
* @param beginDate 起始日期
* @param endDate 结束日期

View File

@ -13,6 +13,7 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.UnsupportedTemporalTypeException;
/**
* {@link TemporalAccessor} 工具类封装
@ -54,7 +55,19 @@ public class TemporalAccessorUtil extends TemporalUtil{
formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
}
try {
return formatter.format(time);
} catch (UnsupportedTemporalTypeException e){
if(time instanceof LocalDate && e.getMessage().contains("HourOfDay")){
// 用户传入LocalDate但是要求格式化带有时间部分转换为LocalDateTime重试
return formatter.format(((LocalDate) time).atStartOfDay());
}else if(time instanceof LocalTime && e.getMessage().contains("YearOfEra")){
// 用户传入LocalTime但是要求格式化带有日期部分转换为LocalDateTime重试
return formatter.format(((LocalTime) time).atDate(LocalDate.now()));
}
throw e;
}
}
/**

View File

@ -1,7 +1,6 @@
package cn.hutool.core.io;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.file.FileCopier;
import cn.hutool.core.io.file.FileMode;
import cn.hutool.core.io.file.FileNameUtil;
@ -173,13 +172,7 @@ public class FileUtil extends PathUtil {
* @return 文件列表
*/
public static List<File> loopFiles(File file, FileFilter fileFilter) {
if (null == file || false == file.exists()) {
return ListUtil.empty();
}
final List<File> fileList = new ArrayList<>();
walkFiles(file, fileList::add);
return fileList;
return loopFiles(file, -1, fileFilter);
}
/**
@ -216,7 +209,7 @@ public class FileUtil extends PathUtil {
* @return 文件列表
* @since 4.6.3
*/
public static List<File> loopFiles(File file, int maxDepth, final FileFilter fileFilter) {
public static List<File> loopFiles(File file, int maxDepth, FileFilter fileFilter) {
return loopFiles(file.toPath(), maxDepth, fileFilter);
}

View File

@ -28,13 +28,8 @@ import java.io.PushbackReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Objects;
@ -48,25 +43,7 @@ import java.util.zip.Checksum;
*
* @author xiaoleilu
*/
public class IoUtil {
/**
* 默认缓存大小 8192
*/
public static final int DEFAULT_BUFFER_SIZE = 2 << 12;
/**
* 默认中等缓存大小 16384
*/
public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13;
/**
* 默认大缓存大小 32768
*/
public static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14;
/**
* 数据流末尾
*/
public static final int EOF = -1;
public class IoUtil extends NioUtil{
// -------------------------------------------------------------------------------------- Copy start
@ -195,21 +172,6 @@ public class IoUtil {
return size;
}
/**
* 拷贝流 thanks to: https://github.com/venusdrogon/feilong-io/blob/master/src/main/java/com/feilong/io/IOWriteUtil.java<br>
* 本方法不会关闭流
*
* @param in 输入流
* @param out 输出流
* @param bufferSize 缓存大小
* @param streamProgress 进度条
* @return 传输的byte数
* @throws IORuntimeException IO异常
*/
public static long copyByNIO(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {
return copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, streamProgress);
}
/**
* 拷贝文件流使用NIO
*
@ -227,79 +189,13 @@ public class IoUtil {
try {
inChannel = in.getChannel();
outChannel = out.getChannel();
return inChannel.transferTo(0, inChannel.size(), outChannel);
} catch (IOException e) {
throw new IORuntimeException(e);
return copy(inChannel, outChannel);
} finally {
close(outChannel);
close(inChannel);
}
}
/**
* 拷贝流使用NIO不会关闭流
*
* @param in {@link ReadableByteChannel}
* @param out {@link WritableByteChannel}
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
* @since 4.5.0
*/
public static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException {
return copy(in, out, DEFAULT_BUFFER_SIZE);
}
/**
* 拷贝流使用NIO不会关闭流
*
* @param in {@link ReadableByteChannel}
* @param out {@link WritableByteChannel}
* @param bufferSize 缓冲大小如果小于等于0使用默认
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
* @since 4.5.0
*/
public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException {
return copy(in, out, bufferSize, null);
}
/**
* 拷贝流使用NIO不会关闭流
*
* @param in {@link ReadableByteChannel}
* @param out {@link WritableByteChannel}
* @param bufferSize 缓冲大小如果小于等于0使用默认
* @param streamProgress {@link StreamProgress}进度处理器
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
*/
public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {
Assert.notNull(in, "InputStream is null !");
Assert.notNull(out, "OutputStream is null !");
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize <= 0 ? DEFAULT_BUFFER_SIZE : bufferSize);
long size = 0;
if (null != streamProgress) {
streamProgress.start();
}
try {
while (in.read(byteBuffer) != EOF) {
byteBuffer.flip();// 写转读
size += out.write(byteBuffer);
byteBuffer.clear();
if (null != streamProgress) {
streamProgress.progress(size);
}
}
} catch (IOException e) {
throw new IORuntimeException(e);
}
if (null != streamProgress) {
streamProgress.finish();
}
return size;
}
// -------------------------------------------------------------------------------------- Copy end
// -------------------------------------------------------------------------------------- getReader and getWriter start
@ -455,22 +351,7 @@ public class IoUtil {
* @throws IORuntimeException IO异常
*/
public static String read(InputStream in, Charset charset) throws IORuntimeException {
FastByteArrayOutputStream out = read(in);
return null == charset ? out.toString() : out.toString(charset);
}
/**
* 从流中读取内容读取完毕后并不关闭流
*
* @param channel 可读通道读取完毕后并不关闭通道
* @param charset 字符集
* @return 内容
* @throws IORuntimeException IO异常
* @since 4.5.0
*/
public static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException {
FastByteArrayOutputStream out = read(channel);
return null == charset ? out.toString() : out.toString(charset);
return StrUtil.str(readBytes(in), charset);
}
/**
@ -486,19 +367,6 @@ public class IoUtil {
return out;
}
/**
* 从流中读取内容读到输出流中
*
* @param channel 可读通道读取完毕后并不关闭通道
* @return 输出流
* @throws IORuntimeException IO异常
*/
public static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException {
final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
copy(channel, Channels.newChannel(out));
return out;
}
/**
* 从Reader中读取String读取完毕后关闭Reader
*
@ -535,47 +403,6 @@ public class IoUtil {
return builder.toString();
}
/**
* 从FileChannel中读取UTF-8编码内容
*
* @param fileChannel 文件管道
* @return 内容
* @throws IORuntimeException IO异常
*/
public static String readUtf8(FileChannel fileChannel) throws IORuntimeException {
return read(fileChannel, CharsetUtil.CHARSET_UTF_8);
}
/**
* 从FileChannel中读取内容读取完毕后并不关闭Channel
*
* @param fileChannel 文件管道
* @param charsetName 字符集
* @return 内容
* @throws IORuntimeException IO异常
*/
public static String read(FileChannel fileChannel, String charsetName) throws IORuntimeException {
return read(fileChannel, CharsetUtil.charset(charsetName));
}
/**
* 从FileChannel中读取内容
*
* @param fileChannel 文件管道
* @param charset 字符集
* @return 内容
* @throws IORuntimeException IO异常
*/
public static String read(FileChannel fileChannel, Charset charset) throws IORuntimeException {
MappedByteBuffer buffer;
try {
buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).load();
} catch (IOException e) {
throw new IORuntimeException(e);
}
return StrUtil.str(buffer, charset);
}
/**
* 从流中读取bytes读取完毕后关闭流
*
@ -597,12 +424,19 @@ public class IoUtil {
* @since 5.0.4
*/
public static byte[] readBytes(InputStream in, boolean isCloseStream) throws IORuntimeException {
final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
copy(in, out);
if (isCloseStream) {
close(in);
final InputStream availableStream = toAvailableStream(in);
try{
final int available = availableStream.available();
if(available > 0){
byte[] result = new byte[available];
//noinspection ResultOfMethodCallIgnored
availableStream.read(result);
return result;
}
return out.toByteArray();
} catch (IOException e){
throw new IORuntimeException(e);
}
return new byte[0];
}
/**
@ -966,6 +800,42 @@ public class IoUtil {
return (in instanceof PushbackInputStream) ? (PushbackInputStream) in : new PushbackInputStream(in, pushBackSize);
}
/**
* 将指定{@link InputStream} 转换为{@link InputStream#available()}方法可用的流<br>
* 在Socket通信流中服务端未返回数据情况下{@link InputStream#available()}方法始终为{@code 0}<br>
* 因此在读取前需要调用{@link InputStream#read()}读取一个字节未返回会阻塞一旦读取到了{@link InputStream#available()}方法就正常了<br>
* 此方法返回对象的规则为
*
* <ul>
* <li>FileInputStream 返回原对象因为文件流的available方法本身可用</li>
* <li>其它InputStream 返回PushbackInputStream</li>
* </ul>
*
* @param in 被转换的流
* @return 转换后的流可能为{@link PushbackInputStream}
* @since 5.5.3
*/
public static InputStream toAvailableStream(InputStream in) {
if(in instanceof FileInputStream){
// FileInputStream本身支持available方法
return in;
}
final PushbackInputStream pushbackInputStream = toPushbackStream(in, 1);
try {
final int available = pushbackInputStream.available();
if (available <= 0) {
//此操作会阻塞直到有数据被读到
int b = pushbackInputStream.read();
pushbackInputStream.unread(b);
}
} catch (IOException e) {
throw new IORuntimeException(e);
}
return pushbackInputStream;
}
/**
* 将byte[]写到流中
*
@ -1113,22 +983,6 @@ public class IoUtil {
}
}
/**
* 关闭<br>
* 关闭失败不会抛出异常
*
* @param closeable 被关闭的对象
*/
public static void close(AutoCloseable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
/**
* 尝试关闭指定对象<br>
* 判断对象如果实现了{@link AutoCloseable}则调用之

View File

@ -0,0 +1,227 @@
package cn.hutool.core.io;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
/**
* NIO相关工具封装主要针对Channel读写拷贝等封装
*
* @author looly
* @since 5.5.3
*/
public class NioUtil {
/**
* 默认缓存大小 8192
*/
public static final int DEFAULT_BUFFER_SIZE = 2 << 12;
/**
* 默认中等缓存大小 16384
*/
public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13;
/**
* 默认大缓存大小 32768
*/
public static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14;
/**
* 数据流末尾
*/
public static final int EOF = -1;
/**
* 拷贝流 thanks to: https://github.com/venusdrogon/feilong-io/blob/master/src/main/java/com/feilong/io/IOWriteUtil.java<br>
* 本方法不会关闭流
*
* @param in 输入流
* @param out 输出流
* @param bufferSize 缓存大小
* @param streamProgress 进度条
* @return 传输的byte数
* @throws IORuntimeException IO异常
*/
public static long copyByNIO(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {
return copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, streamProgress);
}
/**
* 拷贝文件Channel使用NIO拷贝后不会关闭channel
*
* @param inChannel {@link FileChannel}
* @param outChannel {@link FileChannel}
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
* @since 5.5.3
*/
public static long copy(FileChannel inChannel, FileChannel outChannel) throws IORuntimeException {
Assert.notNull(inChannel, "In channel is null!");
Assert.notNull(outChannel, "Out channel is null!");
try {
return inChannel.transferTo(0, inChannel.size(), outChannel);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 拷贝流使用NIO不会关闭channel
*
* @param in {@link ReadableByteChannel}
* @param out {@link WritableByteChannel}
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
* @since 4.5.0
*/
public static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException {
return copy(in, out, DEFAULT_BUFFER_SIZE);
}
/**
* 拷贝流使用NIO不会关闭channel
*
* @param in {@link ReadableByteChannel}
* @param out {@link WritableByteChannel}
* @param bufferSize 缓冲大小如果小于等于0使用默认
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
* @since 4.5.0
*/
public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException {
return copy(in, out, bufferSize, null);
}
/**
* 拷贝流使用NIO不会关闭channel
*
* @param in {@link ReadableByteChannel}
* @param out {@link WritableByteChannel}
* @param bufferSize 缓冲大小如果小于等于0使用默认
* @param streamProgress {@link StreamProgress}进度处理器
* @return 拷贝的字节数
* @throws IORuntimeException IO异常
*/
public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException {
Assert.notNull(in, "InputStream is null !");
Assert.notNull(out, "OutputStream is null !");
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize <= 0 ? DEFAULT_BUFFER_SIZE : bufferSize);
long size = 0;
if (null != streamProgress) {
streamProgress.start();
}
try {
while (in.read(byteBuffer) != EOF) {
byteBuffer.flip();// 写转读
size += out.write(byteBuffer);
byteBuffer.clear();
if (null != streamProgress) {
streamProgress.progress(size);
}
}
} catch (IOException e) {
throw new IORuntimeException(e);
}
if (null != streamProgress) {
streamProgress.finish();
}
return size;
}
/**
* 从流中读取内容读取完毕后并不关闭流
*
* @param channel 可读通道读取完毕后并不关闭通道
* @param charset 字符集
* @return 内容
* @throws IORuntimeException IO异常
* @since 4.5.0
*/
public static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException {
FastByteArrayOutputStream out = read(channel);
return null == charset ? out.toString() : out.toString(charset);
}
/**
* 从流中读取内容读到输出流中
*
* @param channel 可读通道读取完毕后并不关闭通道
* @return 输出流
* @throws IORuntimeException IO异常
*/
public static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException {
final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
copy(channel, Channels.newChannel(out));
return out;
}
/**
* 从FileChannel中读取UTF-8编码内容
*
* @param fileChannel 文件管道
* @return 内容
* @throws IORuntimeException IO异常
*/
public static String readUtf8(FileChannel fileChannel) throws IORuntimeException {
return read(fileChannel, CharsetUtil.CHARSET_UTF_8);
}
/**
* 从FileChannel中读取内容读取完毕后并不关闭Channel
*
* @param fileChannel 文件管道
* @param charsetName 字符集
* @return 内容
* @throws IORuntimeException IO异常
*/
public static String read(FileChannel fileChannel, String charsetName) throws IORuntimeException {
return read(fileChannel, CharsetUtil.charset(charsetName));
}
/**
* 从FileChannel中读取内容
*
* @param fileChannel 文件管道
* @param charset 字符集
* @return 内容
* @throws IORuntimeException IO异常
*/
public static String read(FileChannel fileChannel, Charset charset) throws IORuntimeException {
MappedByteBuffer buffer;
try {
buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).load();
} catch (IOException e) {
throw new IORuntimeException(e);
}
return StrUtil.str(buffer, charset);
}
/**
* 关闭<br>
* 关闭失败不会抛出异常
*
* @param closeable 被关闭的对象
*/
public static void close(AutoCloseable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
}

View File

@ -0,0 +1,34 @@
package cn.hutool.core.lang;
/**
* 片段默认实现
*
* @param <T> 数字类型用于表示位置index
* @author looly
* @since 5.5.3
*/
public class DefaultSegment<T extends Number> implements Segment<T> {
protected T startIndex;
protected T endIndex;
/**
* 构造
* @param startIndex 起始位置
* @param endIndex 结束位置
*/
public DefaultSegment(T startIndex, T endIndex) {
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public T getStartIndex() {
return this.startIndex;
}
@Override
public T getEndIndex() {
return this.endIndex;
}
}

View File

@ -0,0 +1,41 @@
package cn.hutool.core.lang;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import java.lang.reflect.Type;
/**
* 片段表示用于表示文本集合等数据结构的一个区间
* @param <T> 数字类型用于表示位置index
*
* @author looly
* @since 5.5.3
*/
public interface Segment<T extends Number> {
/**
* 获取起始位置
*
* @return 起始位置
*/
T getStartIndex();
/**
* 获取结束位置
*
* @return 结束位置
*/
T getEndIndex();
/**
* 片段长度默认计算方法为abs({@link #getEndIndex()} - {@link #getEndIndex()})
*
* @return 片段长度
*/
default T length(){
final T start = Assert.notNull(getStartIndex(), "Start index must be not null!");
final T end = Assert.notNull(getEndIndex(), "End index must be not null!");
return Convert.convert((Type) start.getClass(), NumberUtil.sub(end, start).abs());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -239,7 +239,7 @@ public class StrBuilder implements CharSequence, Appendable, Serializable {
*/
public StrBuilder insert(int index, CharSequence csq) {
if (null == csq) {
csq = "null";
csq = StrUtil.EMPTY;
}
int len = csq.length();
moveDataAfterIndex(index, csq.length());
@ -523,7 +523,8 @@ public class StrBuilder implements CharSequence, Appendable, Serializable {
* @param minimumCapacity 最小容量
*/
private void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > value.length) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
expandCapacity(minimumCapacity);
}
}
@ -535,8 +536,9 @@ public class StrBuilder implements CharSequence, Appendable, Serializable {
* @param minimumCapacity 需要扩展的最小容量
*/
private void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity < minimumCapacity) {
int newCapacity = (value.length << 1) + 2;
// overflow-conscious code
if (newCapacity - minimumCapacity < 0) {
newCapacity = minimumCapacity;
}
if (newCapacity < 0) {

View File

@ -510,7 +510,7 @@ public class IdcardUtil {
}
/**
* 根据身份编号获取户籍省份只支持15或18位身份证号码
* 根据身份编号获取市级编码只支持15或18位身份证号码
*
* @param idcard 身份编码
* @return 市级编码
@ -658,9 +658,9 @@ public class IdcardUtil {
}
/**
* 获取省份代
* 获取市级编
*
* @return 省份代
* @return 市级编
*/
public String getCityCode() {
return this.cityCode;

View File

@ -383,7 +383,7 @@ public class ObjectUtil {
* 克隆对象<br>
* 如果对象实现Cloneable接口调用其clone方法<br>
* 如果实现Serializable接口执行深度克隆<br>
* 否则返回<code>null</code>
* 否则返回{@code null}
*
* @param <T> 对象类型
* @param obj 被克隆对象
@ -606,11 +606,24 @@ public class ObjectUtil {
return ArrayUtil.emptyCount(objs);
}
/**
* 是否存在{@code null}对象通过{@link ObjectUtil#isNull(Object)} 判断元素
*
* @param objs 被检查对象
* @return 是否存在
* @since 5.5.3
* @see ArrayUtil#hasNull(Object[])
*/
public static boolean hasNull(Object... objs) {
return ArrayUtil.hasNull(objs);
}
/**
* 是否存在{@code null}或空对象通过{@link ObjectUtil#isEmpty(Object)} 判断元素
*
* @param objs 被检查对象
* @return 是否存在
* @see ArrayUtil#hasEmpty(Object...)
*/
public static boolean hasEmpty(Object... objs) {
return ArrayUtil.hasEmpty(objs);

View File

@ -1,5 +1,8 @@
package cn.hutool.core.util;
import cn.hutool.core.lang.DefaultSegment;
import cn.hutool.core.lang.Segment;
/**
* 分页工具类
*
@ -135,6 +138,35 @@ public class PageUtil {
return new int[]{start, getEndByStart(start, pageSize)};
}
/**
* 将页数和每页条目数转换为开始位置和结束位置<br>
* 此方法用于包括结束位置的分页方法<br>
* 例如
*
* <pre>
* 页码0每页10 = [0, 10]
* 页码1每页10 = [10, 20]
*
* </pre>
*
* <p>
* {@link #setFirstPageNo(int)}设置为1时
* <pre>
* 页码1每页10 = [0, 10]
* 页码2每页10 = [10, 20]
*
* </pre>
*
* @param pageNo 页码从0计数
* @param pageSize 每页条目数
* @return {@link Segment}
* @since 5.5.3
*/
public static Segment<Integer> toSegment(int pageNo, int pageSize) {
final int[] startEnd = transToStartEnd(pageNo, pageSize);
return new DefaultSegment<>(startEnd[0], startEnd[1]);
}
/**
* 根据总数计算总页数
*

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,36 @@ import java.util.Map;
*/
public class XmlUtil {
/**
* 字符串常量XML 空格转义 {@code "&nbsp;" -> " "}
*/
public static final String NBSP = "&nbsp;";
/**
* 字符串常量XML And 符转义 {@code "&amp;" -> "&"}
*/
public static final String AMP = "&amp;";
/**
* 字符串常量XML 双引号转义 {@code "&quot;" -> "\""}
*/
public static final String QUOTE = "&quot;";
/**
* 字符串常量XML 单引号转义 {@code "&apos" -> "'"}
*/
public static final String APOS = "&apos;";
/**
* 字符串常量XML 小于号转义 {@code "&lt;" -> "<"}
*/
public static final String LT = "&lt;";
/**
* 字符串常量XML 大于号转义 {@code "&gt;" -> ">"}
*/
public static final String GT = "&gt;";
/**
* 在XML中无效的字符 正则
*/

View File

@ -0,0 +1,23 @@
package cn.hutool.core.date;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDate;
import java.time.LocalTime;
public class TemporalAccessorUtilTest {
@Test
public void formatLocalDateTest(){
final String format = TemporalAccessorUtil.format(LocalDate.of(2020, 12, 7), DatePattern.NORM_DATETIME_PATTERN);
Assert.assertEquals("2020-12-07 00:00:00", format);
}
@Test
public void formatLocalTimeTest(){
final String today = TemporalAccessorUtil.format(LocalDate.now(), DatePattern.NORM_DATE_PATTERN);
final String format = TemporalAccessorUtil.format(LocalTime.MIN, DatePattern.NORM_DATETIME_PATTERN);
Assert.assertEquals(today + " 00:00:00", format);
}
}

View File

@ -75,6 +75,12 @@ public class IdcardUtilTest {
Assert.assertEquals(province2, "内蒙古");
}
@Test
public void getCityCodeByIdCardTest() {
String codeByIdCard = IdcardUtil.getCityCodeByIdCard(ID_18);
Assert.assertEquals("32108", codeByIdCard);
}
@Test
public void getGenderByIdCardTest() {
int gender = IdcardUtil.getGenderByIdCard(ID_18);

View File

@ -1,5 +1,6 @@
package cn.hutool.db;
import cn.hutool.core.lang.Segment;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.PageUtil;
import cn.hutool.db.sql.Order;
@ -12,7 +13,7 @@ import java.util.Arrays;
*
* @author Looly
*/
public class Page implements Serializable {
public class Page implements Segment<Integer>, Serializable {
private static final long serialVersionUID = 97792549823353462L;
public static final int DEFAULT_PAGE_SIZE = 20;
@ -159,15 +160,27 @@ public class Page implements Serializable {
/**
* @return 开始位置
* @see #getStartIndex()
*/
public int getStartPosition() {
return getStartIndex();
}
@Override
public Integer getStartIndex() {
return PageUtil.getStart(this.pageNumber, this.pageSize);
}
/**
* @return 结束位置
* @see #getEndIndex()
*/
public int getEndPosition() {
return getEndIndex();
}
@Override
public Integer getEndIndex() {
return PageUtil.getEnd(this.pageNumber, this.pageSize);
}

View File

@ -1,5 +1,6 @@
package cn.hutool.db.sql;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Editor;
import cn.hutool.core.util.ArrayUtil;
@ -98,7 +99,7 @@ public class Wrapper {
//对于Oracle这类数据库表名中包含用户名需要单独拆分包装
if(field.contains(StrUtil.DOT)){
final Collection<String> target = CollectionUtil.filter(StrUtil.split(field, StrUtil.C_DOT), (Editor<String>) t -> StrUtil.format("{}{}{}", preWrapQuote, t, sufWrapQuote));
final Collection<String> target = CollUtil.filter(StrUtil.split(field, StrUtil.C_DOT, 2), (Editor<String>) t -> StrUtil.format("{}{}{}", preWrapQuote, t, sufWrapQuote));
return CollectionUtil.join(target, StrUtil.DOT);
}

View File

@ -0,0 +1,61 @@
package cn.hutool.dfa;
import cn.hutool.core.lang.DefaultSegment;
/**
* <p>
* 匹配到的单词包含单词text中匹配单词的内容以及匹配内容在text中的下标
* 下标可以用来做单词的进一步处理如果替换成**
*
* @author 肖海斌
*/
public class FoundWord extends DefaultSegment<Integer> {
/**
* 生效的单词即单词树中的词
*/
private final String word;
/**
* 单词匹配到的内容即文中的单词
*/
private final String foundWord;
/**
* 构造
*
* @param word 生效的单词即单词树中的词
* @param foundWord 单词匹配到的内容即文中的单词
* @param startIndex 起始位置包含
* @param endIndex 结束位置包含
*/
public FoundWord(String word, String foundWord, int startIndex, int endIndex) {
super(startIndex, endIndex);
this.word = word;
this.foundWord = foundWord;
}
/**
* 获取生效的单词即单词树中的词
*
* @return 生效的单词
*/
public String getWord() {
return word;
}
/**
* 获取单词匹配到的内容即文中的单词
* @return 单词匹配到的内容
*/
public String getFoundWord() {
return foundWord;
}
/**
* 默认的只输出匹配到的关键字
* @return 匹配到的关键字
*/
@Override
public String toString() {
return this.foundWord;
}
}

View File

@ -0,0 +1,22 @@
package cn.hutool.dfa;
/**
* @author 肖海斌
* 敏感词过滤处理器默认按字符数替换成*
*/
public interface SensitiveProcessor {
/**
* 敏感词过滤处理
* @param foundWord 敏感词匹配到的内容
* @return 敏感词过滤后的内容默认按字符数替换成*
*/
default String process(FoundWord foundWord) {
int length = foundWord.getFoundWord().length();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append("*");
}
return sb.toString();
}
}

View File

@ -1,17 +1,20 @@
package cn.hutool.dfa;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 敏感词工具类
* @author Looly
*
* @author Looly
*/
public final class SensitiveUtil {
@ -22,11 +25,12 @@ public final class SensitiveUtil {
* @return 是否已经被初始化
*/
public static boolean isInited() {
return !sensitiveTree.isEmpty();
return false == sensitiveTree.isEmpty();
}
/**
* 初始化敏感词树
*
* @param isAsync 是否异步初始化
* @param sensitiveWords 敏感词列表
*/
@ -43,6 +47,7 @@ public final class SensitiveUtil {
/**
* 初始化敏感词树
*
* @param sensitiveWords 敏感词列表
*/
public static void init(Collection<String> sensitiveWords) {
@ -53,6 +58,7 @@ public final class SensitiveUtil {
/**
* 初始化敏感词树
*
* @param sensitiveWords 敏感词列表组成的字符串
* @param isAsync 是否异步初始化
* @param separator 分隔符
@ -65,6 +71,7 @@ public final class SensitiveUtil {
/**
* 初始化敏感词树使用逗号分隔每个单词
*
* @param sensitiveWords 敏感词列表组成的字符串
* @param isAsync 是否异步初始化
*/
@ -87,6 +94,7 @@ public final class SensitiveUtil {
/**
* 是否包含敏感词
*
* @param text 文本
* @return 是否包含
*/
@ -96,6 +104,7 @@ public final class SensitiveUtil {
/**
* 是否包含敏感词
*
* @param obj bean会被转为JSON字符串
* @return 是否包含
*/
@ -105,31 +114,88 @@ public final class SensitiveUtil {
/**
* 查找敏感词返回找到的第一个敏感词
*
* @param text 文本
* @return 敏感词
* @deprecated 请使用 {@link #getFoundFirstSensitive(String)}
*/
@Deprecated
public static String getFindedFirstSensitive(String text) {
return sensitiveTree.match(text);
}
/**
* 查找敏感词返回找到的第一个敏感词
*
* @param text 文本
* @return 敏感词
* @since 5.5.3
*/
public static FoundWord getFoundFirstSensitive(String text) {
return sensitiveTree.matchWord(text);
}
/**
* 查找敏感词返回找到的第一个敏感词
*
* @param obj bean会被转为JSON字符串
* @return 敏感词
* @deprecated 请使用 {@link #getFoundFirstSensitive(Object)}
*/
@Deprecated
public static String getFindedFirstSensitive(Object obj) {
return sensitiveTree.match(JSONUtil.toJsonStr(obj));
}
/**
* 查找敏感词返回找到的所有敏感词
* @param text 文本
* 查找敏感词返回找到的第一个敏感词
*
* @param obj bean会被转为JSON字符串
* @return 敏感词
*/
public static FoundWord getFoundFirstSensitive(Object obj) {
return sensitiveTree.matchWord(JSONUtil.toJsonStr(obj));
}
/**
* 查找敏感词返回找到的所有敏感词
*
* @param text 文本
* @return 敏感词
* @deprecated 请使用 {@link #getFoundAllSensitive(String)}
*/
@Deprecated
public static List<String> getFindedAllSensitive(String text) {
return sensitiveTree.matchAll(text);
}
/**
* 查找敏感词返回找到的所有敏感词
*
* @param text 文本
* @return 敏感词
* @since 5.5.3
*/
public static List<FoundWord> getFoundAllSensitive(String text) {
return sensitiveTree.matchAllWords(text);
}
/**
* 查找敏感词返回找到的所有敏感词<br>
* 密集匹配原则假如关键词有 ab,b文本是abab将匹配 [ab,b,ab]<br>
* 贪婪匹配最长匹配原则假如关键字a,ab最长匹配将匹配[a, ab]
*
* @param text 文本
* @param isDensityMatch 是否使用密集匹配原则
* @param isGreedMatch 是否使用贪婪匹配最长匹配原则
* @return 敏感词
* @deprecated 请使用 {@link #getFoundAllSensitive(String, boolean, boolean)}
*/
@Deprecated
public static List<String> getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) {
return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch);
}
/**
* 查找敏感词返回找到的所有敏感词<br>
* 密集匹配原则假如关键词有 ab,b文本是abab将匹配 [ab,b,ab]<br>
@ -140,19 +206,33 @@ public final class SensitiveUtil {
* @param isGreedMatch 是否使用贪婪匹配最长匹配原则
* @return 敏感词
*/
public static List<String> getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch){
return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch);
public static List<FoundWord> getFoundAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) {
return sensitiveTree.matchAllWords(text, -1, isDensityMatch, isGreedMatch);
}
/**
* 查找敏感词返回找到的所有敏感词
*
* @param bean 对象会被转为JSON
* @return 敏感词
* @deprecated 请使用 {@link #getFoundAllSensitive(Object)}
*/
@Deprecated
public static List<String> getFindedAllSensitive(Object bean) {
return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean));
}
/**
* 查找敏感词返回找到的所有敏感词
*
* @param bean 对象会被转为JSON
* @return 敏感词
* @since 5.5.3
*/
public static List<FoundWord> getFoundAllSensitive(Object bean) {
return sensitiveTree.matchAllWords(JSONUtil.toJsonStr(bean));
}
/**
* 查找敏感词返回找到的所有敏感词<br>
* 密集匹配原则假如关键词有 ab,b文本是abab将匹配 [ab,b,ab]<br>
@ -162,8 +242,77 @@ public final class SensitiveUtil {
* @param isDensityMatch 是否使用密集匹配原则
* @param isGreedMatch 是否使用贪婪匹配最长匹配原则
* @return 敏感词
* @deprecated 请使用 {@link #getFoundAllSensitive(Object, boolean, boolean)}
*/
@Deprecated
public static List<String> getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) {
return getFindedAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch);
return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean), -1, isDensityMatch, isGreedMatch);
}
/**
* 查找敏感词返回找到的所有敏感词<br>
* 密集匹配原则假如关键词有 ab,b文本是abab将匹配 [ab,b,ab]<br>
* 贪婪匹配最长匹配原则假如关键字a,ab最长匹配将匹配[a, ab]
*
* @param bean 对象会被转为JSON
* @param isDensityMatch 是否使用密集匹配原则
* @param isGreedMatch 是否使用贪婪匹配最长匹配原则
* @return 敏感词
* @since 5.5.3
*/
public static List<FoundWord> getFoundAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) {
return getFoundAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch);
}
/**
* 敏感词过滤
*
* @param bean 对象会被转为JSON
* @param isGreedMatch 贪婪匹配最长匹配原则假如关键字a,ab最长匹配将匹配[a, ab]
* @param sensitiveProcessor 敏感词处理器默认按匹配内容的字符数替换成*
* @param <T> bean的class类型
* @return 敏感词过滤处理后的bean对象
*/
public static <T> T sensitiveFilter(T bean, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) {
String jsonText = JSONUtil.toJsonStr(bean);
@SuppressWarnings("unchecked")
final Class<T> c = (Class<T>) bean.getClass();
return JSONUtil.toBean(sensitiveFilter(jsonText, isGreedMatch, sensitiveProcessor), c);
}
/**
* 处理过滤文本中的敏感词默认替换成*
*
* @param text 文本
* @param isGreedMatch 贪婪匹配最长匹配原则假如关键字a,ab最长匹配将匹配[a, ab]
* @param sensitiveProcessor 敏感词处理器默认按匹配内容的字符数替换成*
* @return 敏感词过滤处理后的文本
*/
public static String sensitiveFilter(String text, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) {
if (StrUtil.isEmpty(text)) {
return text;
}
//敏感词过滤场景下不需要密集匹配
List<FoundWord> foundWordList = getFoundAllSensitive(text, false, isGreedMatch);
if (CollUtil.isEmpty(foundWordList)) {
return text;
}
sensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() {
} : sensitiveProcessor;
Map<Integer, FoundWord> foundWordMap = new HashMap<>(foundWordList.size());
foundWordList.forEach(foundWord -> foundWordMap.put(foundWord.getStartIndex(), foundWord));
int length = text.length();
StringBuilder textStringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
FoundWord fw = foundWordMap.get(i);
if (fw != null) {
textStringBuilder.append(sensitiveProcessor.process(fw));
i = fw.getEndIndex();
} else {
textStringBuilder.append(text.charAt(i));
}
}
return textStringBuilder.toString();
}
}

View File

@ -1,5 +1,6 @@
package cn.hutool.dfa;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.text.StrBuilder;
@ -31,7 +32,7 @@ public class WordTree extends HashMap<Character, WordTree> {
private static final long serialVersionUID = -4646423269465809276L;
/**
* 敏感词字符末尾标识用于标识单词末尾字符
* 词字符末尾标识用于标识单词末尾字符
*/
private final Set<Character> endCharacterSet = new HashSet<>();
/**
@ -67,26 +68,30 @@ public class WordTree extends HashMap<Character, WordTree> {
* 增加一组单词
*
* @param words 单词集合
* @return this
*/
public void addWords(Collection<String> words) {
public WordTree addWords(Collection<String> words) {
if (false == (words instanceof Set)) {
words = new HashSet<>(words);
}
for (String word : words) {
addWord(word);
}
return this;
}
/**
* 增加一组单词
*
* @param words 单词数组
* @return this
*/
public void addWords(String... words) {
public WordTree addWords(String... words) {
HashSet<String> wordsSet = CollectionUtil.newHashSet(words);
for (String word : wordsSet) {
addWord(word);
}
return this;
}
/**
@ -94,7 +99,7 @@ public class WordTree extends HashMap<Character, WordTree> {
*
* @param word 单词
*/
public void addWord(String word) {
public WordTree addWord(String word) {
final Filter<Character> charFilter = this.charFilter;
WordTree parent = null;
WordTree current = this;
@ -117,8 +122,8 @@ public class WordTree extends HashMap<Character, WordTree> {
if (null != parent) {
parent.setEnd(currentChar);
}
return this;
}
//------------------------------------------------------------------------------- match
/**
@ -131,7 +136,7 @@ public class WordTree extends HashMap<Character, WordTree> {
if (null == text) {
return false;
}
return null != match(text);
return null != matchWord(text);
}
/**
@ -141,14 +146,23 @@ public class WordTree extends HashMap<Character, WordTree> {
* @return 匹配到的关键字
*/
public String match(String text) {
final FoundWord foundWord = matchWord(text);
return null != foundWord ? foundWord.toString() : null;
}
/**
* 获得第一个匹配的关键字
*
* @param text 被检查的文本
* @return 匹配到的关键字
* @since 5.5.3
*/
public FoundWord matchWord(String text) {
if (null == text) {
return null;
}
List<String> matchAll = matchAll(text, 1);
if (CollectionUtil.isNotEmpty(matchAll)) {
return matchAll.get(0);
}
return null;
final List<FoundWord> matchAll = matchAllWords(text, 1);
return CollUtil.get(matchAll, 0);
}
//------------------------------------------------------------------------------- match all
@ -163,6 +177,17 @@ public class WordTree extends HashMap<Character, WordTree> {
return matchAll(text, -1);
}
/**
* 找出所有匹配的关键字
*
* @param text 被检查的文本
* @return 匹配的词列表
* @since 5.5.3
*/
public List<FoundWord> matchAllWords(String text) {
return matchAllWords(text, -1);
}
/**
* 找出所有匹配的关键字
*
@ -174,6 +199,18 @@ public class WordTree extends HashMap<Character, WordTree> {
return matchAll(text, limit, false, false);
}
/**
* 找出所有匹配的关键字
*
* @param text 被检查的文本
* @param limit 限制匹配个数
* @return 匹配的词列表
* @since 5.5.3
*/
public List<FoundWord> matchAllWords(String text, int limit) {
return matchAllWords(text, limit, false, false);
}
/**
* 找出所有匹配的关键字<br>
* 密集匹配原则假如关键词有 ab,b文本是abab将匹配 [ab,b,ab]<br>
@ -186,19 +223,38 @@ public class WordTree extends HashMap<Character, WordTree> {
* @return 匹配的词列表
*/
public List<String> matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) {
final List<FoundWord> matchAllWords = matchAllWords(text, limit, isDensityMatch, isGreedMatch);
return CollUtil.map(matchAllWords, FoundWord::toString, true);
}
/**
* 找出所有匹配的关键字<br>
* 密集匹配原则假如关键词有 ab,b文本是abab将匹配 [ab,b,ab]<br>
* 贪婪匹配最长匹配原则假如关键字a,ab最长匹配将匹配[a, ab]
*
* @param text 被检查的文本
* @param limit 限制匹配个数
* @param isDensityMatch 是否使用密集匹配原则
* @param isGreedMatch 是否使用贪婪匹配最长匹配原则
* @return 匹配的词列表
* @since 5.5.3
*/
public List<FoundWord> matchAllWords(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) {
if (null == text) {
return null;
}
List<String> foundWords = new ArrayList<>();
List<FoundWord> foundWords = new ArrayList<>();
WordTree current = this;
int length = text.length();
final Filter<Character> charFilter = this.charFilter;
//存放查找到的字符缓存完整出现一个词时加到findedWords中否则清空
final StrBuilder wordBuffer = StrUtil.strBuilder();
final StrBuilder keyBuffer = StrUtil.strBuilder();
char currentChar;
for (int i = 0; i < length; i++) {
wordBuffer.reset();
keyBuffer.reset();
for (int j = i; j < length; j++) {
currentChar = text.charAt(j);
// Console.log("i: {}, j: {}, currentChar: {}", i, j, currentChar);
@ -216,9 +272,10 @@ public class WordTree extends HashMap<Character, WordTree> {
break;
}
wordBuffer.append(currentChar);
keyBuffer.append(currentChar);
if (current.isEnd(currentChar)) {
//到达单词末尾关键词成立从此词的下一个位置开始查找
foundWords.add(wordBuffer.toString());
foundWords.add(new FoundWord(keyBuffer.toString(), wordBuffer.toString(), i, j));
if (limit > 0 && foundWords.size() >= limit) {
//超过匹配限制个数直接返回
return foundWords;
@ -241,8 +298,6 @@ public class WordTree extends HashMap<Character, WordTree> {
}
return foundWords;
}
//--------------------------------------------------------------------------------------- Private method start
/**

View File

@ -1,12 +1,10 @@
package cn.hutool.dfa.test;
import java.util.List;
package cn.hutool.dfa;
import cn.hutool.core.collection.CollUtil;
import org.junit.Assert;
import org.junit.Test;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.dfa.WordTree;
import java.util.List;
/**
* DFA单元测试
@ -29,7 +27,7 @@ public class DfaTest {
// 匹配到就不再继续匹配了因此大土豆不匹配
// 匹配到刚出锅就跳过这三个字了因此出锅不匹配由于刚首先被匹配因此长的被匹配最短匹配只针对第一个字相同选最短
List<String> matchAll = tree.matchAll(text, -1, false, false);
Assert.assertEquals(matchAll, CollectionUtil.newArrayList("", "土^豆", "刚出锅"));
Assert.assertEquals(matchAll, CollUtil.newArrayList("", "土^豆", "刚出锅"));
}
/**
@ -45,7 +43,7 @@ public class DfaTest {
// 被匹配最短匹配原则大土豆被跳过土豆继续被匹配
// 刚出锅被匹配由于不跳过已经匹配的词出锅被匹配
List<String> matchAll = tree.matchAll(text, -1, true, false);
Assert.assertEquals(matchAll, CollectionUtil.newArrayList("", "土^豆", "刚出锅", "出锅"));
Assert.assertEquals(matchAll, CollUtil.newArrayList("", "土^豆", "刚出锅", "出锅"));
}
/**
@ -61,7 +59,7 @@ public class DfaTest {
// 匹配到由于到最长匹配因此大土豆接着被匹配
// 由于大土豆被匹配土豆被跳过由于刚出锅被匹配出锅被跳过
List<String> matchAll = tree.matchAll(text, -1, false, true);
Assert.assertEquals(matchAll, CollectionUtil.newArrayList("", "大土^豆", "刚出锅"));
Assert.assertEquals(matchAll, CollUtil.newArrayList("", "大土^豆", "刚出锅"));
}
@ -78,7 +76,7 @@ public class DfaTest {
// 匹配到由于到最长匹配因此大土豆接着被匹配由于不跳过已经匹配的关键词土豆继续被匹配
// 刚出锅被匹配由于不跳过已经匹配的词出锅被匹配
List<String> matchAll = tree.matchAll(text, -1, true, true);
Assert.assertEquals(matchAll, CollectionUtil.newArrayList("", "大土^豆", "土^豆", "刚出锅", "出锅"));
Assert.assertEquals(matchAll, CollUtil.newArrayList("", "大土^豆", "土^豆", "刚出锅", "出锅"));
}
@ -91,7 +89,7 @@ public class DfaTest {
tree.addWord("tio");
List<String> all = tree.matchAll("AAAAAAAt-ioBBBBBBB");
Assert.assertEquals(all, CollectionUtil.newArrayList("t-io"));
Assert.assertEquals(all, CollUtil.newArrayList("t-io"));
}
@Test

View File

@ -0,0 +1,48 @@
package cn.hutool.dfa;
import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class SensitiveUtilTest {
@Test
public void testSensitiveFilter() {
List<String> wordList = new ArrayList<>();
wordList.add("");
wordList.add("大土豆");
wordList.add("土豆");
wordList.add("刚出锅");
wordList.add("出锅");
TestBean bean = new TestBean();
bean.setStr("我有一颗$大土^豆,刚出锅的");
bean.setNum(100);
SensitiveUtil.init(wordList);
bean = SensitiveUtil.sensitiveFilter(bean, true, null);
Assert.assertEquals(bean.getStr(), "我有一颗$*******的");
}
public static class TestBean {
private String str;
private Integer num;
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
}
}

View File

@ -130,6 +130,13 @@
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>262</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<!-- 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>

View File

@ -0,0 +1,7 @@
/**
* Spring相关工具封装
*
* @author looly
*
*/
package cn.hutool.extra.spring;

View File

@ -0,0 +1,130 @@
package cn.hutool.extra.ssh;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import ch.ethz.ssh2.StreamGobbler;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
/**
* Ganymed-SSH2封装http://www.ganymed.ethz.ch/ssh2/
*
* @author looly
* @since 5.5.3
*/
public class GanymedUtil {
/**
* 连接到服务器
*
* @param sshHost 主机
* @param sshPort 端口
* @return {@link Connection}
*/
public static Connection connect(String sshHost, int sshPort) {
Connection conn = new Connection(sshHost, sshPort);
try {
conn.connect();
} catch (IOException e) {
throw new IORuntimeException(e);
}
return conn;
}
/**
* 打开远程会话
*
* @param sshHost 主机
* @param sshPort 端口
* @param sshUser 用户名如果为null默认root
* @param sshPass 密码
* @return {@link Session}
*/
public static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass) {
// 默认root用户
if (StrUtil.isEmpty(sshUser)) {
sshUser = "root";
}
final Connection connect = connect(sshHost, sshPort);
try {
connect.authenticateWithPassword(sshUser, sshPass);
return connect.openSession();
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 执行Shell命令使用EXEC方式
* <p>
* 此方法单次发送一个命令到服务端不读取环境变量执行结束后自动关闭Session不会产生阻塞
* </p>
*
* @param session Session会话
* @param cmd 命令
* @param charset 发送和读取内容的编码
* @param errStream 错误信息输出到的位置
*/
public static String exec(Session session, String cmd, Charset charset, OutputStream errStream) {
final String result;
try {
session.execCommand(cmd, charset.name());
result = IoUtil.read(new StreamGobbler(session.getStdout()), charset);
// 错误输出
IoUtil.copy(new StreamGobbler(session.getStdout()), errStream);
} catch (IOException e) {
throw new IORuntimeException(e);
} finally {
close(session);
}
return result;
}
/**
* 执行Shell命令
* <p>
* 此方法单次发送一个命令到服务端自动读取环境变量执行结束后自动关闭Session不会产生阻塞
* </p>
*
* @param session Session会话
* @param cmd 命令
* @param charset 发送和读取内容的编码
* @param errStream 错误信息输出到的位置
*/
public static String execByShell(Session session, String cmd, Charset charset, OutputStream errStream) {
final String result;
try {
session.requestDumbPTY();
IoUtil.write(session.getStdin(), charset, true, cmd);
result = IoUtil.read(new StreamGobbler(session.getStdout()), charset);
if(null != errStream){
// 错误输出
IoUtil.copy(new StreamGobbler(session.getStdout()), errStream);
}
} catch (IOException e) {
throw new IORuntimeException(e);
} finally {
close(session);
}
return result;
}
/**
* 关闭会话
*
* @param session 会话通道
*/
public static void close(Session session) {
if (session != null) {
session.close();
}
}
}

View File

@ -417,7 +417,7 @@ public class JschUtil {
* @param cmd 命令
* @param charset 发送和读取内容的编码
* @param errStream 错误信息输出到的位置
* @return {@link ChannelExec}
* @return 执行结果内容
* @since 4.3.1
*/
public static String exec(Session session, String cmd, Charset charset, OutputStream errStream) {
@ -432,7 +432,7 @@ public class JschUtil {
try {
channel.connect();
in = channel.getInputStream();
return IoUtil.read(in, CharsetUtil.CHARSET_UTF_8);
return IoUtil.read(in, charset);
} catch (IOException e) {
throw new IORuntimeException(e);
} catch (JSchException e) {
@ -461,7 +461,6 @@ public class JschUtil {
shell.setPty(true);
OutputStream out = null;
InputStream in = null;
final StringBuilder result = StrUtil.builder();
try {
out = shell.getOutputStream();
in = shell.getInputStream();
@ -469,9 +468,7 @@ public class JschUtil {
out.write(StrUtil.bytes(cmd, charset));
out.flush();
while (in.available() > 0) {
result.append(IoUtil.read(in, charset));
}
return IoUtil.read(in, charset);
} catch (IOException e) {
throw new IORuntimeException(e);
} finally {
@ -479,7 +476,6 @@ public class JschUtil {
IoUtil.close(in);
close(shell);
}
return result.toString();
}
/**

View File

@ -10,9 +10,9 @@ import org.springframework.test.context.junit4.SpringRunner;
* @author sidian
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EnableSprintUtilTest.class)
@SpringBootTest(classes = EnableSpringUtilTest.class)
@EnableSpringUtil
public class EnableSprintUtilTest {
public class EnableSpringUtilTest {
@Test
public void test() {

View File

@ -240,11 +240,14 @@ final class InternalJSONUtil {
*/
private static String formatDate(Object dateObj, String format) {
if (StrUtil.isNotBlank(format)) {
final String dateStr;
if(dateObj instanceof TemporalAccessor){
return TemporalAccessorUtil.format((TemporalAccessor) dateObj, format);
dateStr = TemporalAccessorUtil.format((TemporalAccessor) dateObj, format);
} else{
dateStr = DateUtil.format(Convert.toDate(dateObj), format);
}
//用户定义了日期格式
return JSONUtil.quote(DateUtil.format(Convert.toDate(dateObj), format));
return JSONUtil.quote(dateStr);
}
//默认使用时间戳

View File

@ -115,7 +115,8 @@ public class JSONConfig implements Serializable {
}
/**
* 设置日期格式null表示默认的时间戳
* 设置日期格式null表示默认的时间戳<br>
* 此方法设置的日期格式仅对转换为JSON字符串有效对解析JSON为bean无效
*
* @param dateFormat 日期格式null表示默认的时间戳
* @return this

View File

@ -20,10 +20,10 @@ public interface JSONGetter<K> extends OptNullBasicTypeFromObjectGetter<K> {
JSONConfig getConfig();
/**
* key对应值是否为<code>null</code>或无此key
* key对应值是否为{@code null}或无此key
*
* @param key
* @return true 无此key或值为<code>null</code>{@link JSONNull#NULL}返回<code>false</code>其它返回<code>true</code>
* @return true 无此key或值为{@code null}{@link JSONNull#NULL}返回{@code false}其它返回{@code true}
*/
default boolean isNull(K key) {
return JSONNull.NULL.equals(this.getObj(key));

View File

@ -261,7 +261,8 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
}
/**
* 设置转为字符串时的日期格式默认为时间戳null值
* 设置转为字符串时的日期格式默认为时间戳null值<br>
* 此方法设置的日期格式仅对转换为JSON字符串有效对解析JSON为bean无效
*
* @param format 格式null表示使用时间戳
* @return this
@ -341,7 +342,7 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
}
/**
* PUT 键值对到JSONObject中在忽略null模式下如果值为<code>null</code>将此键移除
* PUT 键值对到JSONObject中在忽略null模式下如果值为{@code null}将此键移除
*
* @param key
* @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.
@ -356,7 +357,7 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
}
/**
* 设置键值对到JSONObject中在忽略null模式下如果值为<code>null</code>将此键移除
* 设置键值对到JSONObject中在忽略null模式下如果值为{@code null}将此键移除
*
* @param key
* @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL.
@ -426,7 +427,7 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
* @param key
* @param value 被积累的值
* @return this.
* @throws JSONException 如果给定键为<code>null</code>或者键对应的值存在且为非JSONArray
* @throws JSONException 如果给定键为{@code null}或者键对应的值存在且为非JSONArray
*/
public JSONObject accumulate(String key, Object value) throws JSONException {
InternalJSONUtil.testValidity(value);
@ -447,7 +448,7 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
* @param key
* @param value
* @return this.
* @throws JSONException 如果给定键为<code>null</code>或者键对应的值存在且为非JSONArray
* @throws JSONException 如果给定键为{@code null}或者键对应的值存在且为非JSONArray
*/
public JSONObject append(String key, Object value) throws JSONException {
InternalJSONUtil.testValidity(value);
@ -545,7 +546,7 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
/**
* 返回JSON字符串<br>
* 如果解析错误返回<code>null</code>
* 如果解析错误返回{@code null}
*
* @return JSON字符串
*/

View File

@ -9,7 +9,7 @@ import cn.hutool.core.util.StrUtil;
* @author looly
* @since 3.1.2
*/
public class JSONStrFormater {
public class JSONStrFormatter {
/** 单位缩进字符串。*/
private static final String SPACE = " ";

View File

@ -694,12 +694,12 @@ public final class JSONUtil {
* 在需要的时候包装对象<br>
* 包装包括
* <ul>
* <li><code>null</code> = <code>JSONNull.NULL</code></li>
* <li>{@code null} = {@code JSONNull.NULL}</li>
* <li>array or collection = JSONArray</li>
* <li>map = JSONObject</li>
* <li>standard property (Double, String, et al) = 原对象</li>
* <li>来自于java包 = 字符串</li>
* <li>其它 = 尝试包装为JSONObject否则返回<code>null</code></li>
* <li>其它 = 尝试包装为JSONObject否则返回{@code null}</li>
* </ul>
*
* @param object 被包装的对象
@ -776,7 +776,7 @@ public final class JSONUtil {
* @since 3.1.2
*/
public static String formatJsonStr(String jsonStr) {
return JSONStrFormater.format(jsonStr);
return JSONStrFormatter.format(jsonStr);
}
/**

View File

@ -13,21 +13,21 @@ public class JSONStrFormaterTest {
@Test
public void formatTest() {
String json = "{'age':23,'aihao':['pashan','movies'],'name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies','name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies']}]}}";
String result = JSONStrFormater.format(json);
String result = JSONStrFormatter.format(json);
Assert.assertNotNull(result);
}
@Test
public void formatTest2() {
String json = "{\"abc\":{\"def\":\"\\\"[ghi]\"}}";
String result = JSONStrFormater.format(json);
String result = JSONStrFormatter.format(json);
Assert.assertNotNull(result);
}
@Test
public void formatTest3() {
String json = "{\"id\":13,\"title\":\"《标题》\",\"subtitle\":\"副标题z'c'z'xv'c'xv\",\"user_id\":6,\"type\":0}";
String result = JSONStrFormater.format(json);
String result = JSONStrFormatter.format(json);
Assert.assertNotNull(result);
}
}

View File

@ -2,30 +2,19 @@ package cn.hutool.poi.excel.sax;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.cell.FormulaCellValue;
import cn.hutool.poi.excel.sax.handler.RowHandler;
import cn.hutool.poi.exceptions.POIException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.ss.usermodel.BuiltinFormats;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Sax方式读取Excel文件<br>
@ -34,50 +23,11 @@ import java.util.List;
* @author Looly
* @since 3.1.2
*/
public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<Excel07SaxReader> {
public class Excel07SaxReader implements ExcelSaxReader<Excel07SaxReader> {
// sheet r:Id前缀
private static final String RID_PREFIX = "rId";
// 单元格的格式表对应style.xml
private StylesTable stylesTable;
// excel 2007 的共享字符串表,对应sharedString.xml
private SharedStringsTable sharedStringsTable;
// sheet的索引
private int sheetIndex;
// 当前非空行
private int index;
// 当前列
private int curCell;
// 单元数据类型
private CellDataType cellDataType;
// 当前行号从0开始
private long rowNumber;
// 当前列坐标 如A1B5
private String curCoordinate;
// 当前节点名称
private ElementName curElementName;
// 前一个列的坐标
private String preCoordinate;
// 行的最大列坐标
private String maxCellCoordinate;
// 单元格样式
private XSSFCellStyle xssfCellStyle;
// 单元格存储的格式化字符串nmtFmt的formatCode属性的值
private String numFmtString;
// 上一次的内容
private final StrBuilder lastContent = StrUtil.strBuilder();
// 上一次的内容
private final StrBuilder lastFormula = StrUtil.strBuilder();
// 存储每行的列元素
private List<Object> rowCellList = new ArrayList<>();
/**
* 行处理器
*/
private RowHandler rowHandler;
private final SheetDataSaxHandler handler;
/**
* 构造
@ -85,7 +35,7 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<E
* @param rowHandler 行处理器
*/
public Excel07SaxReader(RowHandler rowHandler) {
this.rowHandler = rowHandler;
this.handler = new SheetDataSaxHandler(rowHandler);
}
/**
@ -95,7 +45,7 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<E
* @return this
*/
public Excel07SaxReader setRowHandler(RowHandler rowHandler) {
this.rowHandler = rowHandler;
this.handler.setRowHandler(rowHandler);
return this;
}
@ -170,16 +120,16 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<E
* @since 5.4.4
*/
public Excel07SaxReader read(XSSFReader xssfReader, String idOrRid) throws POIException {
// 获取共享样式表
// 获取共享样式表样式非必须
try {
stylesTable = xssfReader.getStylesTable();
} catch (Exception e) {
this.handler.stylesTable = xssfReader.getStylesTable();
} catch (IOException | InvalidFormatException ignore) {
// ignore
}
// 获取共享字符串表
try {
this.sharedStringsTable = xssfReader.getSharedStringsTable();
this.handler.sharedStringsTable = xssfReader.getSharedStringsTable();
} catch (IOException e) {
throw new IORuntimeException(e);
} catch (InvalidFormatException e) {
@ -190,62 +140,6 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<E
}
// ------------------------------------------------------------------------------ Read end
/**
* 读到一个xml开始标签时的回调处理方法
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
final ElementName name = ElementName.of(localName);
this.curElementName = name;
if(null != name){
switch (name){
case row:
// 行开始
startRow(attributes);
break;
case c:
// 单元格元素
startCell(attributes);
break;
}
}
}
/**
* 标签结束的回调处理方法
*/
@Override
public void endElement(String uri, String localName, String qName) {
this.curElementName = null;
if (ElementName.c.match(localName)) { // 单元格结束
endCell();
} else if (ElementName.row.match(localName)) {// 行结束
endRow();
}
}
/**
* s标签结束的回调处理方法
*/
@Override
public void characters(char[] ch, int start, int length) {
final ElementName elementName = this.curElementName;
if(null != elementName){
switch (elementName){
case v:
// 得到单元格内容的值
lastContent.append(ch, start, length);
break;
case f:
// 得到单元格内容的值
lastFormula.append(ch, start, length);
break;
}
}
// 其它标签忽略
}
// --------------------------------------------------------------------------------------- Private method start
/**
@ -266,25 +160,25 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<E
idOrRid = rid;
}
}
this.sheetIndex = Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRid, RID_PREFIX));
this.handler.sheetIndex = Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRid, RID_PREFIX));
InputStream sheetInputStream = null;
try {
if (this.sheetIndex > -1) {
if (this.handler.sheetIndex > -1) {
// 根据 rId# rSheet# 查找sheet
sheetInputStream = xssfReader.getSheet(RID_PREFIX + (this.sheetIndex + 1));
ExcelSaxUtil.readFrom(sheetInputStream, this);
rowHandler.doAfterAllAnalysed();
sheetInputStream = xssfReader.getSheet(RID_PREFIX + (this.handler.sheetIndex + 1));
ExcelSaxUtil.readFrom(sheetInputStream, this.handler);
this.handler.rowHandler.doAfterAllAnalysed();
} else {
this.sheetIndex = -1;
this.handler.sheetIndex = -1;
// 遍历所有sheet
final Iterator<InputStream> sheetInputStreams = xssfReader.getSheetsData();
while (sheetInputStreams.hasNext()) {
// 重新读取一个sheet时行归零
index = 0;
this.sheetIndex++;
this.handler.index = 0;
this.handler.sheetIndex++;
sheetInputStream = sheetInputStreams.next();
ExcelSaxUtil.readFrom(sheetInputStream, this);
rowHandler.doAfterAllAnalysed();
ExcelSaxUtil.readFrom(sheetInputStream, this.handler);
this.handler.rowHandler.doAfterAllAnalysed();
}
}
} catch (RuntimeException e) {
@ -296,140 +190,5 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader<E
}
return this;
}
/**
* 行开始
*
* @param attributes 属性列表
*/
private void startRow(Attributes attributes) {
this.rowNumber = Long.parseLong(AttributeName.r.getValue(attributes)) - 1;
}
/**
* 单元格开始
*
* @param attributes 属性列表
*/
private void startCell(Attributes attributes) {
// 获取当前列坐标
final String tempCurCoordinate = AttributeName.r.getValue(attributes);
// 前一列为null则将其设置为"@",A为第一列ascii码为65前一列即为@ascii码64
if (preCoordinate == null) {
preCoordinate = String.valueOf(ExcelSaxUtil.CELL_FILL_CHAR);
} else {
// 存在则前一列要设置为上一列的坐标
preCoordinate = curCoordinate;
}
// 重置当前列
curCoordinate = tempCurCoordinate;
// 设置单元格类型
setCellType(attributes);
// 清空之前的数据
lastContent.reset();
lastFormula.reset();
}
/**
* 一个单元格结尾
*/
private void endCell() {
// 补全单元格之间的空格
fillBlankCell(preCoordinate, curCoordinate, false);
final String contentStr = StrUtil.trim(lastContent);
Object value = ExcelSaxUtil.getDataValue(this.cellDataType, contentStr, this.sharedStringsTable, this.numFmtString);
if(false == this.lastFormula.isEmpty()){
value = new FormulaCellValue(StrUtil.trim(lastFormula), value);
}
addCellValue(curCell++, value);
}
/**
* 一行结尾
*/
private void endRow() {
// 最大列坐标以第一个非空行的为准
if (index == 0) {
maxCellCoordinate = curCoordinate;
}
// 补全一行尾部可能缺失的单元格
if (maxCellCoordinate != null) {
fillBlankCell(curCoordinate, maxCellCoordinate, true);
}
rowHandler.handle(sheetIndex, rowNumber, rowCellList);
// 一行结束
// 新建一个新列之前的列抛弃可能被回收或rowHandler处理
rowCellList = new ArrayList<>(curCell + 1);
// 行数增加
index++;
// 当前列置0
curCell = 0;
// 置空当前列坐标和前一列坐标
curCoordinate = null;
preCoordinate = null;
}
/**
* 在一行中的指定列增加值
*
* @param index 位置
* @param value
*/
private void addCellValue(int index, Object value) {
this.rowCellList.add(index, value);
this.rowHandler.handleCell(this.sheetIndex, this.rowNumber, index, value, this.xssfCellStyle);
}
/**
* 填充空白单元格如果前一个单元格大于后一个不需要填充<br>
*
* @param preCoordinate 前一个单元格坐标
* @param curCoordinate 当前单元格坐标
* @param isEnd 是否为最后一个单元格
*/
private void fillBlankCell(String preCoordinate, String curCoordinate, boolean isEnd) {
if (false == curCoordinate.equals(preCoordinate)) {
int len = ExcelSaxUtil.countNullCell(preCoordinate, curCoordinate);
if (isEnd) {
len++;
}
while (len-- > 0) {
addCellValue(curCell++, "");
}
}
}
/**
* 设置单元格的类型
*
* @param attributes 属性
*/
private void setCellType(Attributes attributes) {
// numFmtString的值
numFmtString = StrUtil.EMPTY;
this.cellDataType = CellDataType.of(AttributeName.t.getValue(attributes));
// 获取单元格的xf索引对应style.xml中cellXfs的子元素xf
if (null != this.stylesTable) {
final String xfIndexStr = AttributeName.s.getValue(attributes);
if (null != xfIndexStr) {
this.xssfCellStyle = stylesTable.getStyleAt(Integer.parseInt(xfIndexStr));
// 单元格存储格式的索引对应style.xml中的numFmts元素的子元素索引
final int numFmtIndex = xssfCellStyle.getDataFormat();
this.numFmtString = ObjectUtil.defaultIfNull(
xssfCellStyle.getDataFormatString(),
BuiltinFormats.getBuiltinFormat(numFmtIndex));
if (CellDataType.NUMBER == this.cellDataType && ExcelSaxUtil.isDateFormat(numFmtIndex, numFmtString)) {
cellDataType = CellDataType.DATE;
}
}
}
}
// --------------------------------------------------------------------------------------- Private method end
}

View File

@ -0,0 +1,307 @@
package cn.hutool.poi.excel.sax;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.cell.FormulaCellValue;
import cn.hutool.poi.excel.sax.handler.RowHandler;
import org.apache.poi.ss.usermodel.BuiltinFormats;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList;
import java.util.List;
/**
* sheetData标签内容读取处理器
*
* <pre>
* &lt;sheetData&gt;&lt;/sheetData&gt;
* </pre>
* @since 5.5.3
*/
public class SheetDataSaxHandler extends DefaultHandler {
// 单元格的格式表对应style.xml
protected StylesTable stylesTable;
// excel 2007 的共享字符串表,对应sharedString.xml
protected SharedStringsTable sharedStringsTable;
// sheet的索引
protected int sheetIndex;
// 当前非空行
protected int index;
// 当前列
private int curCell;
// 单元数据类型
private CellDataType cellDataType;
// 当前行号从0开始
private long rowNumber;
// 当前列坐标 如A1B5
private String curCoordinate;
// 当前节点名称
private ElementName curElementName;
// 前一个列的坐标
private String preCoordinate;
// 行的最大列坐标
private String maxCellCoordinate;
// 单元格样式
private XSSFCellStyle xssfCellStyle;
// 单元格存储的格式化字符串nmtFmt的formatCode属性的值
private String numFmtString;
// 是否处于sheetData标签内sax只解析此标签内的内容其它标签忽略
private boolean isInSheetData;
// 上一次的内容
private final StrBuilder lastContent = StrUtil.strBuilder();
// 上一次的内容
private final StrBuilder lastFormula = StrUtil.strBuilder();
// 存储每行的列元素
private List<Object> rowCellList = new ArrayList<>();
public SheetDataSaxHandler(RowHandler rowHandler){
this.rowHandler = rowHandler;
}
/**
* 行处理器
*/
protected RowHandler rowHandler;
/**
* 设置行处理器
*
* @param rowHandler 行处理器
*/
public void setRowHandler(RowHandler rowHandler) {
this.rowHandler = rowHandler;
}
/**
* 读到一个xml开始标签时的回调处理方法
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if ("sheetData".equals(qName)) {
this.isInSheetData = true;
return;
}
if (false == this.isInSheetData) {
// 非sheetData标签忽略解析
return;
}
final ElementName name = ElementName.of(qName);
this.curElementName = name;
if (null != name) {
switch (name) {
case row:
// 行开始
startRow(attributes);
break;
case c:
// 单元格元素
startCell(attributes);
break;
}
}
}
/**
* 标签结束的回调处理方法
*/
@Override
public void endElement(String uri, String localName, String qName) {
if ("sheetData".equals(qName)) {
// sheetData结束不再解析别的标签
this.isInSheetData = false;
return;
}
if (false == this.isInSheetData) {
// 非sheetData标签忽略解析
return;
}
this.curElementName = null;
if (ElementName.c.match(qName)) { // 单元格结束
endCell();
} else if (ElementName.row.match(qName)) {// 行结束
endRow();
}
// 其它标签忽略
}
/**
* s标签结束的回调处理方法
*/
@Override
public void characters(char[] ch, int start, int length) {
if (false == this.isInSheetData) {
// 非sheetData标签忽略解析
return;
}
final ElementName elementName = this.curElementName;
if (null != elementName) {
switch (elementName) {
case v:
// 得到单元格内容的值
lastContent.append(ch, start, length);
break;
case f:
// 得到单元格内容的值
lastFormula.append(ch, start, length);
break;
}
}
// 其它标签忽略
}
// --------------------------------------------------------------------------------------- Private method start
/**
* 行开始
*
* @param attributes 属性列表
*/
private void startRow(Attributes attributes) {
final String rValue = AttributeName.r.getValue(attributes);
if (null != rValue) {
this.rowNumber = Long.parseLong(rValue) - 1;
}
}
/**
* 单元格开始
*
* @param attributes 属性列表
*/
private void startCell(Attributes attributes) {
// 获取当前列坐标
final String tempCurCoordinate = AttributeName.r.getValue(attributes);
// 前一列为null则将其设置为"@",A为第一列ascii码为65前一列即为@ascii码64
if (preCoordinate == null) {
preCoordinate = String.valueOf(ExcelSaxUtil.CELL_FILL_CHAR);
} else {
// 存在则前一列要设置为上一列的坐标
preCoordinate = curCoordinate;
}
// 重置当前列
curCoordinate = tempCurCoordinate;
// 设置单元格类型
setCellType(attributes);
// 清空之前的数据
lastContent.reset();
lastFormula.reset();
}
/**
* 一行结尾
*/
private void endRow() {
// 最大列坐标以第一个非空行的为准
if (index == 0) {
maxCellCoordinate = curCoordinate;
}
// 补全一行尾部可能缺失的单元格
if (maxCellCoordinate != null) {
fillBlankCell(curCoordinate, maxCellCoordinate, true);
}
rowHandler.handle(sheetIndex, rowNumber, rowCellList);
// 一行结束
// 新建一个新列之前的列抛弃可能被回收或rowHandler处理
rowCellList = new ArrayList<>(curCell + 1);
// 行数增加
index++;
// 当前列置0
curCell = 0;
// 置空当前列坐标和前一列坐标
curCoordinate = null;
preCoordinate = null;
}
/**
* 一个单元格结尾
*/
private void endCell() {
// 补全单元格之间的空格
fillBlankCell(preCoordinate, curCoordinate, false);
final String contentStr = StrUtil.trim(lastContent);
Object value = ExcelSaxUtil.getDataValue(this.cellDataType, contentStr, this.sharedStringsTable, this.numFmtString);
if (false == this.lastFormula.isEmpty()) {
value = new FormulaCellValue(StrUtil.trim(lastFormula), value);
}
addCellValue(curCell++, value);
}
/**
* 在一行中的指定列增加值
*
* @param index 位置
* @param value
*/
private void addCellValue(int index, Object value) {
this.rowCellList.add(index, value);
this.rowHandler.handleCell(this.sheetIndex, this.rowNumber, index, value, this.xssfCellStyle);
}
/**
* 填充空白单元格如果前一个单元格大于后一个不需要填充<br>
*
* @param preCoordinate 前一个单元格坐标
* @param curCoordinate 当前单元格坐标
* @param isEnd 是否为最后一个单元格
*/
private void fillBlankCell(String preCoordinate, String curCoordinate, boolean isEnd) {
if (false == curCoordinate.equals(preCoordinate)) {
int len = ExcelSaxUtil.countNullCell(preCoordinate, curCoordinate);
if (isEnd) {
len++;
}
while (len-- > 0) {
addCellValue(curCell++, StrUtil.EMPTY);
}
}
}
/**
* 设置单元格的类型
*
* @param attributes 属性
*/
private void setCellType(Attributes attributes) {
// numFmtString的值
numFmtString = StrUtil.EMPTY;
this.cellDataType = CellDataType.of(AttributeName.t.getValue(attributes));
// 获取单元格的xf索引对应style.xml中cellXfs的子元素xf
if (null != this.stylesTable) {
final String xfIndexStr = AttributeName.s.getValue(attributes);
if (null != xfIndexStr) {
this.xssfCellStyle = stylesTable.getStyleAt(Integer.parseInt(xfIndexStr));
// 单元格存储格式的索引对应style.xml中的numFmts元素的子元素索引
final int numFmtIndex = xssfCellStyle.getDataFormat();
this.numFmtString = ObjectUtil.defaultIfNull(
xssfCellStyle.getDataFormatString(),
BuiltinFormats.getBuiltinFormat(numFmtIndex));
if (CellDataType.NUMBER == this.cellDataType && ExcelSaxUtil.isDateFormat(numFmtIndex, numFmtString)) {
cellDataType = CellDataType.DATE;
}
}
}
}
// --------------------------------------------------------------------------------------- Private method end
}

View File

@ -175,4 +175,12 @@ public class ExcelSaxReadTest {
ExcelUtil.getReader(file).read().forEach(Console::log);
}
@Test
@Ignore
public void readXlsmTest(){
ExcelUtil.readBySax("d:/test/WhiteListTemplate.xlsm", -1, (sheetIndex, rowIndex, rowlist) -> {
Console.log("[{}] [{}] {}", sheetIndex, rowIndex, rowlist);
});
}
}