diff --git a/CHANGELOG.md b/CHANGELOG.md index 469cfb88e..0a5619315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,20 @@ # 🚀Changelog ------------------------------------------------------------------------------------------------------------- -# 5.8.0 (2022-03-20) +# 5.8.0 (2022-03-22) ### ❌不兼容特性 -* 【db 】 【不向下兼容】增加MongoDB4.x支持(pr#568@Gitee) +* 【db 】 【不向下兼容 】增加MongoDB4.x支持返回MongoClient变更(pr#568@Gitee) * 【json 】 【可能兼容问题】修改JSONObject结构,继承自MapWrapper * 【core 】 【可能兼容问题】BeanCopier重构,新建XXXCopier,删除XXXValueProvider * 【core 】 【可能兼容问题】URLEncoder废弃,URLEncoderUtil使用RFC3986 -* 【core 】 【可能兼容问题】增加Base32.encode不足位数补= +* 【core 】 【可能兼容问题】Base32分离编码和解码,以便减少数据加载,支持Hex模式 +* 【core 】 【可能兼容问题】Base58分离编码和解码 +* 【core 】 【可能兼容问题】Base62分离编码和解码,增加inverted模式支持 +* 【core 】 【兼容问题 】PunyCode参数由String改为Charsequence +* 【cron 】 【可能兼容问题】SimpleValueParser改名为AbsValueParser,改为abstract +* 【poi 】 【可能兼容问题】ExcelUtil.getBigWriter返回值改为BigExcelWriter +* 【core 】 【可能兼容问题】Opt.ofEmptyAble参数由List改为Collection子类(pr#580@Gitee) ### 🐣新特性 * 【http 】 HttpRequest.form采用TableMap方式(issue#I4W427@Gitee) @@ -30,6 +36,8 @@ * 【poi 】 优化ExcelBase,将alias放入 * 【poi 】 优化ExcelBase,将alias放入 * 【core 】 改进StrUtil#startWith、endWith性能 +* 【cron 】 增加CronPatternParser、MatcherTable +* 【http 】 GlobalHeaders增加系统属性allowUnsafeServerCertChange、allowUnsafeRenegotiation ### 🐞Bug修复 * 【core 】 修复ObjectUtil.hasNull传入null返回true的问题(pr#555@Gitee) @@ -41,6 +49,7 @@ * 【core 】 修复CopyOptions中fieldNameEditor无效问题(issue#2202@Github) * 【json 】 修复JSON对Map.Entry的解析问题 * 【core 】 修复MapConverter中map与map转换兼容问题 +* 【poi 】 解决sax读取时,POI-5.2.x兼容性问题 ------------------------------------------------------------------------------------------------------------- # 5.7.22 (2022-03-01) diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java index 1fa7db4cb..80c87b7e3 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java @@ -6,85 +6,28 @@ import cn.hutool.core.util.StrUtil; import java.nio.charset.Charset; /** - * Base32 - encodes and decodes RFC3548 Base32 (see http://www.faqs.org/rfcs/rfc3548.html )
+ * Base32 - encodes and decodes RFC4648 Base32 (see https://datatracker.ietf.org/doc/html/rfc4648#section-6 )
* base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。
- * 所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。 - * see http://blog.csdn.net/earbao/article/details/44453937 - * @author Looly + * 所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。
+ * 根据RFC4648 Base32规范,支持两种模式: + * * + * @author Looly */ public class Base32 { - - private Base32() {} - - private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - private static final int[] BASE32_LOOKUP = {// - 0xFF, 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, // '0', '1', '2', '3', '4', '5', '6', '7' - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // '8', '9', ':', ';', '<', '=', '>', '?' - 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' - 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O' - 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W' - 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_' - 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g' - 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' - 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' - 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' - }; - private static final int[] BASE32_FILL = {-1, 4, 1, 6, 3}; - //----------------------------------------------------------------------------------------- encode + /** * 编码 + * * @param bytes 数据 * @return base32 */ public static String encode(final byte[] bytes) { - int i = 0; - int index = 0; - int digit; - int currByte; - int nextByte; - - int encodeLen = bytes.length * 8 / 5; - if (encodeLen != 0) { - encodeLen = encodeLen + 1 + BASE32_FILL[(bytes.length * 8) % 5]; - } - - StringBuilder base32 = new StringBuilder(encodeLen); - - while (i < bytes.length) { - // unsign - currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); - - /* Is the current digit going to span a byte boundary? */ - if (index > 3) { - if ((i + 1) < bytes.length) { - nextByte = (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256); - } else { - nextByte = 0; - } - - digit = currByte & (0xFF >> index); - index = (index + 5) % 8; - digit <<= index; - digit |= nextByte >> (8 - index); - i++; - } else { - digit = (currByte >> (8 - (index + 5))) & 0x1F; - index = (index + 5) % 8; - if (index == 0) { - i++; - } - } - base32.append(BASE32_CHARS.charAt(digit)); - } - - // 末尾补充不足长度的 - while(base32.length() < encodeLen){ - base32.append('='); - } - - return base32.toString(); + return Base32Codec.INSTANCE.encode(bytes); } /** @@ -100,18 +43,7 @@ public class Base32 { /** * base32编码 * - * @param source 被编码的base32字符串 - * @param charset 字符集 - * @return 被加密后的字符串 - */ - public static String encode(String source, String charset) { - return encode(StrUtil.bytes(source, charset)); - } - - /** - * base32编码 - * - * @param source 被编码的base32字符串 + * @param source 被编码的base32字符串 * @param charset 字符集 * @return 被加密后的字符串 */ @@ -119,55 +51,47 @@ public class Base32 { return encode(StrUtil.bytes(source, charset)); } + /** + * 编码 + * + * @param bytes 数据(Hex模式) + * @return base32 + */ + public static String encodeHex(final byte[] bytes) { + return Base32Codec.INSTANCE.encode(bytes, true); + } + + /** + * base32编码(Hex模式) + * + * @param source 被编码的base32字符串 + * @return 被加密后的字符串 + */ + public static String encodeHex(String source) { + return encodeHex(source, CharsetUtil.CHARSET_UTF_8); + } + + /** + * base32编码(Hex模式) + * + * @param source 被编码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encodeHex(String source, Charset charset) { + return encodeHex(StrUtil.bytes(source, charset)); + } + //----------------------------------------------------------------------------------------- decode + /** * 解码 + * * @param base32 base32编码 * @return 数据 */ - public static byte[] decode(final String base32) { - int i, index, lookup, offset, digit; - int len = base32.endsWith("=") ? base32.indexOf("=") * 5 / 8 : base32.length() * 5 / 8; - byte[] bytes = new byte[len]; - - for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { - lookup = base32.charAt(i) - '0'; - - /* Skip chars outside the lookup table */ - if (lookup < 0 || lookup >= BASE32_LOOKUP.length) { - continue; - } - - digit = BASE32_LOOKUP[lookup]; - - /* If this digit is not in the table, ignore it */ - if (digit == 0xFF) { - continue; - } - - if (index <= 3) { - index = (index + 5) % 8; - if (index == 0) { - bytes[offset] |= digit; - offset++; - if (offset >= bytes.length) { - break; - } - } else { - bytes[offset] |= digit << (8 - index); - } - } else { - index = (index + 5) % 8; - bytes[offset] |= (digit >>> index); - offset++; - - if (offset >= bytes.length) { - break; - } - bytes[offset] |= digit << (8 - index); - } - } - return bytes; + public static byte[] decode(String base32) { + return Base32Codec.INSTANCE.decode(base32); } /** @@ -183,22 +107,42 @@ public class Base32 { /** * base32解码 * - * @param source 被解码的base32字符串 - * @param charset 字符集 - * @return 被加密后的字符串 - */ - public static String decodeStr(String source, String charset) { - return StrUtil.str(decode(source), charset); - } - - /** - * base32解码 - * - * @param source 被解码的base32字符串 + * @param source 被解码的base32字符串 * @param charset 字符集 * @return 被加密后的字符串 */ public static String decodeStr(String source, Charset charset) { return StrUtil.str(decode(source), charset); } + + /** + * 解码 + * + * @param base32 base32编码 + * @return 数据 + */ + public static byte[] decodeHex(String base32) { + return Base32Codec.INSTANCE.decode(base32, true); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @return 被加密后的字符串 + */ + public static String decodeStrHex(String source) { + return decodeStrHex(source, CharsetUtil.CHARSET_UTF_8); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStrHex(String source, Charset charset) { + return StrUtil.str(decodeHex(source), charset); + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base32Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base32Codec.java new file mode 100755 index 000000000..42b926e54 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base32Codec.java @@ -0,0 +1,215 @@ +package cn.hutool.core.codec; + +import java.util.Arrays; + +/** + * Base32 - encodes and decodes RFC4648 Base32 (see https://datatracker.ietf.org/doc/html/rfc4648#section-6 )
+ * base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。
+ * 所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。
+ * 根据RFC4648 Base32规范,支持两种模式: + * + * + * @author Looly + * @since 5.8.0 + */ +public class Base32Codec implements Encoder, Decoder { + + public static Base32Codec INSTANCE = new Base32Codec(); + + @Override + public String encode(byte[] data) { + return encode(data, false); + } + + /** + * 编码数据 + * + * @param data 数据 + * @param useHex 是否使用Hex Alphabet + * @return 编码后的Base32字符串 + */ + public String encode(byte[] data, boolean useHex) { + final Base32Encoder encoder = useHex ? Base32Encoder.HEX_ENCODER : Base32Encoder.ENCODER; + return encoder.encode(data); + } + + @Override + public byte[] decode(CharSequence encoded) { + return decode(encoded, false); + } + + /** + * 解码数据 + * + * @param encoded base32字符串 + * @param useHex 是否使用Hex Alphabet + * @return 解码后的内容 + */ + public byte[] decode(CharSequence encoded, boolean useHex) { + final Base32Decoder decoder = useHex ? Base32Decoder.HEX_DECODER : Base32Decoder.DECODER; + return decoder.decode(encoded); + } + + /** + * Bas32编码器 + */ + public static class Base32Encoder implements Encoder { + private static final String DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final String HEX_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + private static final Character DEFAULT_PAD = '='; + private static final int[] BASE32_FILL = {-1, 4, 1, 6, 3}; + + public static final Base32Encoder ENCODER = new Base32Encoder(DEFAULT_ALPHABET, DEFAULT_PAD); + public static final Base32Encoder HEX_ENCODER = new Base32Encoder(HEX_ALPHABET, DEFAULT_PAD); + + private final char[] alphabet; + private final Character pad; + + /** + * 构造 + * + * @param alphabet 自定义编码字母表,见 {@link #DEFAULT_ALPHABET}和 {@link #HEX_ALPHABET} + * @param pad 补位字符 + */ + public Base32Encoder(String alphabet, Character pad) { + this.alphabet = alphabet.toCharArray(); + this.pad = pad; + } + + @Override + public String encode(byte[] data) { + int i = 0; + int index = 0; + int digit; + int currByte; + int nextByte; + + int encodeLen = data.length * 8 / 5; + if (encodeLen != 0) { + encodeLen = encodeLen + 1 + BASE32_FILL[(data.length * 8) % 5]; + } + + StringBuilder base32 = new StringBuilder(encodeLen); + + while (i < data.length) { + // unsign + currByte = (data[i] >= 0) ? data[i] : (data[i] + 256); + + /* Is the current digit going to span a byte boundary? */ + if (index > 3) { + if ((i + 1) < data.length) { + nextByte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256); + } else { + nextByte = 0; + } + + digit = currByte & (0xFF >> index); + index = (index + 5) % 8; + digit <<= index; + digit |= nextByte >> (8 - index); + i++; + } else { + digit = (currByte >> (8 - (index + 5))) & 0x1F; + index = (index + 5) % 8; + if (index == 0) { + i++; + } + } + base32.append(alphabet[digit]); + } + + if (null != pad) { + // 末尾补充不足长度的 + while (base32.length() < encodeLen) { + base32.append(pad.charValue()); + } + } + + return base32.toString(); + } + } + + /** + * Base32解码器 + */ + public static class Base32Decoder implements Decoder { + private static final char BASE_CHAR = '0'; + + public static final Base32Decoder DECODER = new Base32Decoder(Base32Encoder.DEFAULT_ALPHABET); + public static final Base32Decoder HEX_DECODER = new Base32Decoder(Base32Encoder.HEX_ALPHABET); + + private final byte[] lookupTable; + + /** + * 构造 + * + * @param alphabet 编码字母表 + */ + public Base32Decoder(String alphabet) { + lookupTable = new byte[128]; + Arrays.fill(lookupTable, (byte) -1); + + final int length = alphabet.length(); + + char c; + for (int i = 0; i < length; i++) { + c = alphabet.charAt(i); + lookupTable[c - BASE_CHAR] = (byte) i; + // 支持小写字母解码 + if(c >= 'A' && c <= 'Z'){ + lookupTable[Character.toLowerCase(c) - BASE_CHAR] = (byte) i; + } + } + } + + @Override + public byte[] decode(CharSequence encoded) { + int i, index, lookup, offset, digit; + final String base32 = encoded.toString(); + int len = base32.endsWith("=") ? base32.indexOf("=") * 5 / 8 : base32.length() * 5 / 8; + byte[] bytes = new byte[len]; + + for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { + lookup = base32.charAt(i) - BASE_CHAR; + + /* Skip chars outside the lookup table */ + if (lookup < 0 || lookup >= lookupTable.length) { + continue; + } + + digit = lookupTable[lookup]; + + /* If this digit is not in the table, ignore it */ + if (digit < 0) { + continue; + } + + if (index <= 3) { + index = (index + 5) % 8; + if (index == 0) { + bytes[offset] |= digit; + offset++; + if (offset >= bytes.length) { + break; + } + } else { + bytes[offset] |= digit << (8 - index); + } + } else { + index = (index + 5) % 8; + bytes[offset] |= (digit >>> index); + offset++; + + if (offset >= bytes.length) { + break; + } + bytes[offset] |= digit << (8 - index); + } + } + return bytes; + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java index 6e6d02d82..da278731e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java @@ -15,10 +15,6 @@ public class Base58Codec implements Encoder, Decoder, Decoder= 0) { - encoded[--outputStart] = ENCODED_ZERO; - } - // Return encoded string (including encoded leading zeros). - return new String(encoded, outputStart, encoded.length - outputStart); + return Base58Encoder.ENCODER.encode(data); } /** @@ -68,52 +35,130 @@ public class Base58Codec implements Encoder, Decoder { + private static final String DEFAULT_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public static final Base58Encoder ENCODER = new Base58Encoder(DEFAULT_ALPHABET.toCharArray()); + + private final char[] alphabet; + private final char alphabetZero; + + /** + * 构造 + * + * @param alphabet 编码字母表 + */ + public Base58Encoder(char[] alphabet) { + this.alphabet = alphabet; + alphabetZero = alphabet[0]; + } + + @Override + public String encode(byte[] data) { + if (null == data) { + return null; + } + if (data.length == 0) { + return StrUtil.EMPTY; + } + // 计算开头0的个数 + int zeroCount = 0; + while (zeroCount < data.length && data[zeroCount] == 0) { + ++zeroCount; + } + // 将256位编码转换为58位编码 + data = Arrays.copyOf(data, data.length); // since we modify it in-place + final char[] encoded = new char[data.length * 2]; // upper bound + int outputStart = encoded.length; + for (int inputStart = zeroCount; inputStart < data.length; ) { + encoded[--outputStart] = alphabet[divmod(data, inputStart, 256, 58)]; + if (data[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. + while (outputStart < encoded.length && encoded[outputStart] == alphabetZero) { + ++outputStart; + } + while (--zeroCount >= 0) { + encoded[--outputStart] = alphabetZero; + } + // Return encoded string (including encoded leading zeros). + return new String(encoded, outputStart, encoded.length - outputStart); + } + } + + /** + * Base58解码器 + * + * @since 5.8.0 + */ + public static class Base58Decoder implements Decoder { + + public static Base58Decoder DECODER = new Base58Decoder(Base58Encoder.DEFAULT_ALPHABET); + + private final byte[] lookupTable; + + /** + * 构造 + * + * @param alphabet 编码字符表 + */ + public Base58Decoder(String alphabet) { + final byte[] lookupTable = new byte['z' + 1]; + Arrays.fill(lookupTable, (byte) -1); + + final int length = alphabet.length(); + for (int i = 0; i < length; i++) { + lookupTable[alphabet.charAt(i)] = (byte) i; + } + this.lookupTable = lookupTable; + } + + @Override + public byte[] decode(CharSequence encoded) { + if (encoded.length() == 0) { + return new byte[0]; + } + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + final byte[] input58 = new byte[encoded.length()]; + for (int i = 0; i < encoded.length(); ++i) { + char c = encoded.charAt(i); + int digit = c < 128 ? lookupTable[c] : -1; + if (digit < 0) { + throw new IllegalArgumentException(StrUtil.format("Invalid char '{}' at [{}]", c, i)); + } + input58[i] = (byte) digit; + } + // Count leading zeros. + int zeros = 0; + while (zeros < input58.length && input58[zeros] == 0) { + ++zeros; + } + // Convert base-58 digits to base-256 digits. + byte[] decoded = new byte[encoded.length()]; + int outputStart = decoded.length; + for (int inputStart = zeros; inputStart < input58.length; ) { + decoded[--outputStart] = divmod(input58, inputStart, 58, 256); + if (input58[inputStart] == 0) { + ++inputStart; // optimization - skip leading zeros + } + } + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.length && decoded[outputStart] == 0) { + ++outputStart; + } + // Return decoded data (including original number of leading zeros). + return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); + } } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java index bb0454540..9f8a458b4 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java @@ -1,15 +1,15 @@ package cn.hutool.core.codec; -import java.io.File; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; - import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + /** * Base62工具类,提供Base62的编码和解码方案
* @@ -19,7 +19,6 @@ import cn.hutool.core.util.StrUtil; public class Base62 { private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; - private static final Base62Codec CODEC = Base62Codec.createGmp(); // -------------------------------------------------------------------- encode /** @@ -50,7 +49,7 @@ public class Base62 { * @return 被加密后的字符串 */ public static String encode(byte[] source) { - return new String(CODEC.encode(source)); + return new String(Base62Codec.INSTANCE.encode(source)); } /** @@ -73,6 +72,57 @@ public class Base62 { return encode(FileUtil.readBytes(file)); } + /** + * Base62编码(反转字母表模式) + * + * @param source 被编码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String encodeInverted(CharSequence source) { + return encodeInverted(source, DEFAULT_CHARSET); + } + + /** + * Base62编码(反转字母表模式) + * + * @param source 被编码的Base62字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encodeInverted(CharSequence source, Charset charset) { + return encodeInverted(StrUtil.bytes(source, charset)); + } + + /** + * Base62编码(反转字母表模式) + * + * @param source 被编码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String encodeInverted(byte[] source) { + return new String(Base62Codec.INSTANCE.encode(source, true)); + } + + /** + * Base62编码 + * + * @param in 被编码Base62的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + */ + public static String encodeInverted(InputStream in) { + return encodeInverted(IoUtil.readBytes(in)); + } + + /** + * Base62编码(反转字母表模式) + * + * @param file 被编码Base62的文件 + * @return 被加密后的字符串 + */ + public static String encodeInverted(File file) { + return encodeInverted(FileUtil.readBytes(file)); + } + // -------------------------------------------------------------------- decode /** * Base62解码 @@ -144,6 +194,69 @@ public class Base62 { * @return 解码后的bytes */ public static byte[] decode(byte[] base62bytes) { - return CODEC.decode(base62bytes); + return Base62Codec.INSTANCE.decode(base62bytes); + } + + /** + * Base62解码(反转字母表模式) + * + * @param source 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static String decodeStrInverted(CharSequence source) { + return decodeStrInverted(source, DEFAULT_CHARSET); + } + + /** + * Base62解码(反转字母表模式) + * + * @param source 被解码的Base62字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStrInverted(CharSequence source, Charset charset) { + return StrUtil.str(decodeInverted(source), charset); + } + + /** + * Base62解码(反转字母表模式) + * + * @param Base62 被解码的Base62字符串 + * @param destFile 目标文件 + * @return 目标文件 + */ + public static File decodeToFileInverted(CharSequence Base62, File destFile) { + return FileUtil.writeBytes(decodeInverted(Base62), destFile); + } + + /** + * Base62解码(反转字母表模式) + * + * @param base62Str 被解码的Base62字符串 + * @param out 写出到的流 + * @param isCloseOut 是否关闭输出流 + */ + public static void decodeToStreamInverted(CharSequence base62Str, OutputStream out, boolean isCloseOut) { + IoUtil.write(out, isCloseOut, decodeInverted(base62Str)); + } + + /** + * Base62解码(反转字母表模式) + * + * @param base62Str 被解码的Base62字符串 + * @return 被加密后的字符串 + */ + public static byte[] decodeInverted(CharSequence base62Str) { + return decodeInverted(StrUtil.bytes(base62Str, DEFAULT_CHARSET)); + } + + /** + * 解码Base62(反转字母表模式) + * + * @param base62bytes Base62输入 + * @return 解码后的bytes + */ + public static byte[] decodeInverted(byte[] base62bytes) { + return Base62Codec.INSTANCE.decode(base62bytes, true); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java index 1272406ac..055800dd7 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java @@ -12,73 +12,13 @@ import java.io.Serializable; * @author Looly, Sebastian Ruhleder, sebastian@seruco.io * @since 4.5.9 */ -public class Base62Codec implements Encoder, Decoder, Serializable{ +public class Base62Codec implements Encoder, Decoder, Serializable { private static final long serialVersionUID = 1L; private static final int STANDARD_BASE = 256; private static final int TARGET_BASE = 62; - /** - * GMP风格 - */ - private static final byte[] GMP = { // - '0', '1', '2', '3', '4', '5', '6', '7', // - '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', // - 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', // - 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', // - 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', // - 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', // - 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', // - 'u', 'v', 'w', 'x', 'y', 'z' // - }; - - /** - * 反转风格,即将GMP风格中的大小写做转换 - */ - private static final byte[] INVERTED = { // - '0', '1', '2', '3', '4', '5', '6', '7', // - '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', // - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // - 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', // - 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', // - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', // - 'U', 'V', 'W', 'X', 'Y', 'Z' // - }; - - /** - * 创建GMP风格的Base62编码解码器对象 - * - * @return Base62Codec - */ - public static Base62Codec createGmp() { - return new Base62Codec(GMP); - } - - /** - * 创建Inverted风格的Base62编码解码器对象 - * - * @return Base62Codec - */ - public static Base62Codec createInverted() { - return new Base62Codec(INVERTED); - } - - private final byte[] alphabet; - private final byte[] lookup; - - /** - * 构造 - * - * @param alphabet 自定义字母表 - */ - public Base62Codec(byte[] alphabet) { - this.alphabet = alphabet; - lookup = new byte[256]; - for (int i = 0; i < alphabet.length; i++) { - lookup[alphabet[i]] = (byte) (i & 0xFF); - } - } + public static Base62Codec INSTANCE = new Base62Codec(); /** * 编码指定消息bytes为Base62格式的bytes @@ -88,8 +28,19 @@ public class Base62Codec implements Encoder, Decoder, Decoder { + /** + * GMP风格 + */ + private static final byte[] GMP = { // + '0', '1', '2', '3', '4', '5', '6', '7', // + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', // + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', // + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', // + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', // + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', // + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', // + 'u', 'v', 'w', 'x', 'y', 'z' // + }; + + /** + * 反转风格,即将GMP风格中的大小写做转换 + */ + private static final byte[] INVERTED = { // + '0', '1', '2', '3', '4', '5', '6', '7', // + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', // + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // + 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', // + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', // + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', // + 'U', 'V', 'W', 'X', 'Y', 'Z' // + }; + + public static Base62Encoder GMP_ENCODER = new Base62Encoder(GMP); + public static Base62Encoder INVERTED_ENCODER = new Base62Encoder(INVERTED); + + private final byte[] alphabet; + + /** + * 构造 + * + * @param alphabet 字符表 + */ + public Base62Encoder(byte[] alphabet) { + this.alphabet = alphabet; + } + + @Override + public byte[] encode(byte[] data) { + final byte[] indices = convert(data, STANDARD_BASE, TARGET_BASE); + return translate(indices, alphabet); + } + } + + /** + * Base62解码器 + * + * @since 5.8.0 + */ + public static class Base62Decoder implements Decoder { + + public static Base62Decoder GMP_DECODER = new Base62Decoder(Base62Encoder.GMP); + public static Base62Decoder INVERTED_DECODER = new Base62Decoder(Base62Encoder.INVERTED); + + private final byte[] lookupTable; + + /** + * 构造 + * + * @param alphabet 字母表 + */ + public Base62Decoder(byte[] alphabet) { + lookupTable = new byte['z' + 1]; + for (int i = 0; i < alphabet.length; i++) { + lookupTable[alphabet[i]] = (byte) i; + } + } + + + @Override + public byte[] decode(byte[] encoded) { + final byte[] prepared = translate(encoded, lookupTable); + return convert(prepared, TARGET_BASE, STANDARD_BASE); + } } // region Private Methods + /** * 按照字典转换bytes * - * @param indices 内容 + * @param indices 内容 * @param dictionary 字典 * @return 转换值 */ @@ -125,7 +175,7 @@ public class Base62Codec implements Encoder, Decoder, Decoder -1) { decodeByte = DECODE_TABLE[base64Byte]; if (decodeByte > -1) { @@ -156,19 +158,5 @@ public class Base64Decoder { // padding if reached max position return PADDING; } - - /** - * int包装,使之可变 - * - * @author looly - * - */ - private static class IntWrapper { - int value; - - IntWrapper(int value) { - this.value = value; - } - } // ----------------------------------------------------------------------------------------------- Private end } diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java b/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java index d49a046c4..fd3fc3864 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/PunyCode.java @@ -27,11 +27,11 @@ public class PunyCode { /** * 将内容编码为PunyCode * - * @param input 字符串 + * @param input 字符串 * @return PunyCode字符串 * @throws UtilException 计算异常 */ - public static String encode(String input) throws UtilException { + public static String encode(CharSequence input) throws UtilException { return encode(input, false); } @@ -43,7 +43,7 @@ public class PunyCode { * @return PunyCode字符串 * @throws UtilException 计算异常 */ - public static String encode(String input, boolean withPrefix) throws UtilException { + public static String encode(CharSequence input, boolean withPrefix) throws UtilException { int n = INITIAL_N; int delta = 0; int bias = INITIAL_BIAS; @@ -112,7 +112,7 @@ public class PunyCode { n++; } - if(withPrefix){ + if (withPrefix) { output.insert(0, PUNY_CODE_PREFIX); } return output.toString(); @@ -214,6 +214,7 @@ public class PunyCode { * ... * 35 -> '9' * + * * @param d 输入字符 * @return 转换后的字符 * @throws UtilException 无效字符 @@ -242,6 +243,7 @@ public class PunyCode { * ... * '9' -> 35 * + * * @param c 输入字符 * @return 转换后的字符 * @throws UtilException 无效字符 diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java index 4798c8162..76841f5c8 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java @@ -1756,7 +1756,7 @@ public class DateUtil extends CalendarUtil { * @return 是否闰年 */ public static boolean isLeapYear(int year) { - return new GregorianCalendar().isLeapYear(year); + return Year.isLeap(year); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java b/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java index dad1b5cd0..fcc4d1951 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java @@ -29,7 +29,7 @@ import cn.hutool.core.lang.func.Func0; import cn.hutool.core.lang.func.VoidFunc0; import cn.hutool.core.util.StrUtil; -import java.util.List; +import java.util.Collection; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; @@ -108,7 +108,7 @@ public class Opt { * @return 一个包裹里元素可能为空的 {@code Opt} * @since 5.7.17 */ - public static Opt> ofEmptyAble(List value) { + public static > Opt ofEmptyAble(R value) { return CollectionUtil.isEmpty(value) ? empty() : new Opt<>(value); } diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java b/hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java index e50e80a76..df112f2f0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyFunLoader.java @@ -24,6 +24,18 @@ public class LazyFunLoader extends LazyLoader { */ private Supplier supplier; + /** + * 静态工厂方法,提供语义性与编码便利性 + * @param supplier 用于生成对象的函数 + * @param 对象类型 + * @return 函数式懒加载加载器对象 + * @since 5.8.0 + */ + public static LazyFunLoader on(final Supplier supplier) { + Assert.notNull(supplier, "supplier must be not null!"); + return new LazyFunLoader<>(supplier); + } + /** * 构造 * diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java index dce1dd9fc..ae033437f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java @@ -152,12 +152,12 @@ public class MutableInt extends Number implements Comparable, Mutabl * 相等需同时满足如下条件: *
    *
  1. 非空
  2. - *
  3. 类型为 {@link MutableInt}
  4. + *
  5. 类型为 MutableInt
  6. *
  7. 值相等
  8. *
* * @param obj 比对的对象 - * @return 相同返回true,否则 false + * @return 相同返回true,否则 {@code false} */ @Override public boolean equals(final Object obj) { @@ -176,7 +176,7 @@ public class MutableInt extends Number implements Comparable, Mutabl /** * 比较 * - * @param other 其它 {@link MutableInt} 对象 + * @param other 其它 MutableInt 对象 * @return x==y返回0,x<y返回-1,x>y返回1 */ @Override diff --git a/hutool-core/src/main/java/cn/hutool/core/text/AntPathMatcher.java b/hutool-core/src/main/java/cn/hutool/core/text/AntPathMatcher.java index aabc3e97b..8d0850772 100755 --- a/hutool-core/src/main/java/cn/hutool/core/text/AntPathMatcher.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/AntPathMatcher.java @@ -137,6 +137,7 @@ public class AntPathMatcher { * * @param cachePatterns 是否缓存表达式 * @see #getStringMatcher(String) + * @return this */ public AntPathMatcher setCachePatterns(boolean cachePatterns) { this.cachePatterns = cachePatterns; diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java index a17f2e4ab..5d74654bf 100644 --- a/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java +++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java @@ -1,6 +1,7 @@ package cn.hutool.core.codec; import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; import org.junit.Assert; import org.junit.Test; @@ -14,6 +15,24 @@ public class Base32Test { String decodeStr = Base32.decodeStr(encode); Assert.assertEquals(a, decodeStr); + + // 支持小写模式解码 + decodeStr = Base32.decodeStr(encode.toLowerCase()); + Assert.assertEquals(a, decodeStr); + } + + @Test + public void hexEncodeAndDecodeTest(){ + String a = "伦家是一个非常长的字符串"; + String encode = Base32.encodeHex(StrUtil.utf8Bytes(a)); + Assert.assertEquals("SIUADPDEMRJ9HBV4N20E9E5AT6EPTPDON3KPBFV7JA2EBBCNSUMADP5OM8======", encode); + + String decodeStr = Base32.decodeStrHex(encode); + Assert.assertEquals(a, decodeStr); + + // 支持小写模式解码 + decodeStr = Base32.decodeStrHex(encode.toLowerCase()); + Assert.assertEquals(a, decodeStr); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java index dc528e7af..67c801ef2 100644 --- a/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java +++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java @@ -1,5 +1,6 @@ package cn.hutool.core.codec; +import cn.hutool.core.util.RandomUtil; import org.junit.Assert; import org.junit.Test; @@ -20,4 +21,30 @@ public class Base62Test { String decodeStr = Base62.decodeStr(encode); Assert.assertEquals(a, decodeStr); } + + @Test + public void encodeAndDecodeInvertedTest() { + String a = "伦家是一个非常长的字符串66"; + String encode = Base62.encodeInverted(a); + Assert.assertEquals("17Vku8w4jmg8Dqf8LK9vnNKDmoEwN4RjmVA6f0xSlRRt53IkbNQO", encode); + + String decodeStr = Base62.decodeStrInverted(encode); + Assert.assertEquals(a, decodeStr); + } + + @Test + public void encodeAndDecodeRandomTest() { + String a = RandomUtil.randomString(RandomUtil.randomInt(1000)); + String encode = Base62.encode(a); + String decodeStr = Base62.decodeStr(encode); + Assert.assertEquals(a, decodeStr); + } + + @Test + public void encodeAndDecodeInvertedRandomTest() { + String a = RandomUtil.randomString(RandomUtil.randomInt(1000)); + String encode = Base62.encodeInverted(a); + String decodeStr = Base62.decodeStrInverted(encode); + Assert.assertEquals(a, decodeStr); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/loader/LazyFunLoaderTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/loader/LazyFunLoaderTest.java index 6de9cb529..f8d9888cb 100644 --- a/hutool-core/src/test/java/cn/hutool/core/lang/loader/LazyFunLoaderTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/lang/loader/LazyFunLoaderTest.java @@ -42,4 +42,32 @@ public class LazyFunLoaderTest { Assert.assertFalse(loader.isInitialize()); } + + @Test + public void testOnLoadStaticFactoryMethod1() { + + LazyFunLoader loader = LazyFunLoader.on(BigObject::new); + + Assert.assertNotNull(loader.get()); + Assert.assertTrue(loader.isInitialize()); + + // 对于某些对象,在程序关闭时,需要进行销毁操作 + loader.ifInitialized(BigObject::destroy); + + Assert.assertTrue(loader.get().isDestroy); + } + + @Test + public void testOnLoadStaticFactoryMethod2() { + + LazyFunLoader loader = LazyFunLoader.on(BigObject::new); + + // 若从未使用,则可以避免不必要的初始化 + loader.ifInitialized(it -> { + + Assert.fail(); + it.destroy(); + }); + + } } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java index b026cdd3c..ce05317de 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java @@ -1,25 +1,10 @@ package cn.hutool.cron.pattern; -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.cron.CronException; -import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher; -import cn.hutool.cron.pattern.matcher.DayOfMonthValueMatcher; -import cn.hutool.cron.pattern.matcher.ValueMatcher; -import cn.hutool.cron.pattern.matcher.ValueMatcherBuilder; -import cn.hutool.cron.pattern.parser.DayOfMonthValueParser; -import cn.hutool.cron.pattern.parser.DayOfWeekValueParser; -import cn.hutool.cron.pattern.parser.HourValueParser; -import cn.hutool.cron.pattern.parser.MinuteValueParser; -import cn.hutool.cron.pattern.parser.MonthValueParser; -import cn.hutool.cron.pattern.parser.SecondValueParser; -import cn.hutool.cron.pattern.parser.ValueParser; -import cn.hutool.cron.pattern.parser.YearValueParser; +import cn.hutool.cron.pattern.matcher.MatcherTable; +import cn.hutool.cron.pattern.parser.CronPatternParser; -import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; -import java.util.List; import java.util.TimeZone; /** @@ -32,14 +17,14 @@ import java.util.TimeZone; *
  • :范围:1~12,同时支持不区分大小写的别名:"jan","feb", "mar", "apr", "may","jun", "jul", "aug", "sep","oct", "nov", "dec"
  • *
  • :范围:0 (Sunday)~6(Saturday),7也可以表示周日,同时支持不区分大小写的别名:"sun","mon", "tue", "wed", "thu","fri", "sat","L" 表示周六
  • * - * + *

    * 为了兼容Quartz表达式,同时支持6位和7位表达式,其中:
    * *

      * 当为6位时,第一位表示 ,范围0~59,但是第一位不做匹配
      * 当为7位时,最后一位表示 ,范围1970~2099,但是第7位不做解析,也不做匹配
      * 
    - * + *

    * 当定时任务运行到的时间匹配这些表达式后,任务被启动。
    * 注意: * @@ -47,7 +32,7 @@ import java.util.TimeZone; * 当isMatchSecond为{@code true}时才会匹配秒部分 * 默认都是关闭的 * - * + *

    * 对于每一个子表达式,同样支持以下形式: *

      *
    • * :表示匹配这个位置所有的时间
    • @@ -62,10 +47,10 @@ import java.util.TimeZone; *
        * 间隔(/) > 区间(-) > 列表(,)
        * 
      - * + *

      * 例如 2,3,6/3中,由于“/”优先级高,因此相当于2,3,(6/3),结果与 2,3,6等价
      *
      - * + *

      * 一些例子: *

        *
      • 5 * * * * :每个点钟的5分执行,00:05,01:05……
      • @@ -77,36 +62,11 @@ import java.util.TimeZone; *
      * * @author Looly - * */ public class CronPattern { - private static final ValueParser SECOND_VALUE_PARSER = new SecondValueParser(); - private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser(); - private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser(); - private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser(); - private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser(); - private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser(); - private static final ValueParser YEAR_VALUE_PARSER = new YearValueParser(); - private final String pattern; - - /** 秒字段匹配列表 */ - private final List secondMatchers = new ArrayList<>(); - /** 分字段匹配列表 */ - private final List minuteMatchers = new ArrayList<>(); - /** 时字段匹配列表 */ - private final List hourMatchers = new ArrayList<>(); - /** 每月几号字段匹配列表 */ - private final List dayOfMonthMatchers = new ArrayList<>(); - /** 月字段匹配列表 */ - private final List monthMatchers = new ArrayList<>(); - /** 星期字段匹配列表 */ - private final List dayOfWeekMatchers = new ArrayList<>(); - /** 年字段匹配列表 */ - private final List yearMatchers = new ArrayList<>(); - /** 匹配器个数,取决于复合任务表达式中的单一表达式个数 */ - private int matcherSize; + private final MatcherTable matcherTable; /** * 构造 @@ -115,14 +75,15 @@ public class CronPattern { */ public CronPattern(String pattern) { this.pattern = pattern; - parseGroupPattern(pattern); + this.matcherTable = CronPatternParser.create().parse(pattern); } // --------------------------------------------------------------------------------------- match start + /** * 给定时间是否匹配定时任务表达式 * - * @param millis 时间毫秒数 + * @param millis 时间毫秒数 * @param isMatchSecond 是否匹配秒 * @return 如果匹配返回 {@code true}, 否则返回 {@code false} */ @@ -133,8 +94,8 @@ public class CronPattern { /** * 给定时间是否匹配定时任务表达式 * - * @param timezone 时区 {@link TimeZone} - * @param millis 时间毫秒数 + * @param timezone 时区 {@link TimeZone} + * @param millis 时间毫秒数 * @param isMatchSecond 是否匹配秒 * @return 如果匹配返回 {@code true}, 否则返回 {@code false} */ @@ -147,12 +108,12 @@ public class CronPattern { /** * 给定时间是否匹配定时任务表达式 * - * @param calendar 时间 + * @param calendar 时间 * @param isMatchSecond 是否匹配秒 * @return 如果匹配返回 {@code true}, 否则返回 {@code false} */ public boolean match(GregorianCalendar calendar, boolean isMatchSecond) { - final int second = calendar.get(Calendar.SECOND); + final int second = isMatchSecond ? calendar.get(Calendar.SECOND) : -1; final int minute = calendar.get(Calendar.MINUTE); final int hour = calendar.get(Calendar.HOUR_OF_DAY); final int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); @@ -160,20 +121,7 @@ public class CronPattern { final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日 final int year = calendar.get(Calendar.YEAR); - boolean eval; - for (int i = 0; i < matcherSize; i++) { - eval = ((false == isMatchSecond) || secondMatchers.get(i).match(second)) // 匹配秒(非秒匹配模式下始终返回true) - && minuteMatchers.get(i).match(minute)// 匹配分 - && hourMatchers.get(i).match(hour)// 匹配时 - && isMatchDayOfMonth(dayOfMonthMatchers.get(i), dayOfMonth, month, calendar.isLeapYear(year))// 匹配日 - && monthMatchers.get(i).match(month) // 匹配月 - && dayOfWeekMatchers.get(i).match(dayOfWeek)// 匹配周 - && isMatch(yearMatchers, i, year);// 匹配年 - if (eval) { - return true; - } - } - return false; + return this.matcherTable.match(second, minute, hour, dayOfMonth, month, dayOfWeek, year); } // --------------------------------------------------------------------------------------- match end @@ -181,114 +129,4 @@ public class CronPattern { public String toString() { return this.pattern; } - - // -------------------------------------------------------------------------------------- Private method start - /** - * 是否匹配日(指定月份的第几天) - * - * @param matcher {@link ValueMatcher} - * @param dayOfMonth 日 - * @param month 月 - * @param isLeapYear 是否闰年 - * @return 是否匹配 - */ - private static boolean isMatchDayOfMonth(ValueMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) { - return ((matcher instanceof DayOfMonthValueMatcher) // - ? ((DayOfMonthValueMatcher) matcher).match(dayOfMonth, month, isLeapYear) // - : matcher.match(dayOfMonth)); - } - - /** - * 是否匹配指定的日期时间位置 - * - * @param matchers 匹配器列表 - * @param index 位置 - * @param value 被匹配的值 - * @return 是否匹配 - * @since 4.0.2 - */ - private static boolean isMatch(List matchers, int index, int value) { - return (matchers.size() <= index) || matchers.get(index).match(value); - } - - /** - * 解析复合任务表达式 - * - * @param groupPattern 复合表达式 - */ - private void parseGroupPattern(String groupPattern) { - List patternList = StrUtil.split(groupPattern, '|'); - for (String pattern : patternList) { - parseSinglePattern(pattern); - } - } - - /** - * 解析单一定时任务表达式 - * - * @param pattern 表达式 - */ - private void parseSinglePattern(String pattern) { - final String[] parts = pattern.split("\\s"); - - int offset = 0;// 偏移量用于兼容Quartz表达式,当表达式有6或7项时,第一项为秒 - if (parts.length == 6 || parts.length == 7) { - offset = 1; - } else if (parts.length != 5) { - throw new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern); - } - - // 秒 - if (1 == offset) {// 支持秒的表达式 - try { - this.secondMatchers.add(ValueMatcherBuilder.build(parts[0], SECOND_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'second' field error!", pattern); - } - } else {// 不支持秒的表达式,则第一位按照表达式生成时间的秒数赋值,表示整分匹配 - this.secondMatchers.add(ValueMatcherBuilder.build(String.valueOf(DateUtil.date().second()), SECOND_VALUE_PARSER)); - } - // 分 - try { - this.minuteMatchers.add(ValueMatcherBuilder.build(parts[offset], MINUTE_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'minute' field error!", pattern); - } - // 小时 - try { - this.hourMatchers.add(ValueMatcherBuilder.build(parts[1 + offset], HOUR_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'hour' field error!", pattern); - } - // 每月第几天 - try { - this.dayOfMonthMatchers.add(ValueMatcherBuilder.build(parts[2 + offset], DAY_OF_MONTH_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'day of month' field error!", pattern); - } - // 月 - try { - this.monthMatchers.add(ValueMatcherBuilder.build(parts[3 + offset], MONTH_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'month' field error!", pattern); - } - // 星期几 - try { - this.dayOfWeekMatchers.add(ValueMatcherBuilder.build(parts[4 + offset], DAY_OF_WEEK_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'day of week' field error!", pattern); - } - // 年 - if (parts.length == 7) {// 支持年的表达式 - try { - this.yearMatchers.add(ValueMatcherBuilder.build(parts[6], YEAR_VALUE_PARSER)); - } catch (Exception e) { - throw new CronException(e, "Invalid pattern [{}], parsing 'year' field error!", pattern); - } - } else {// 不支持年的表达式,全部匹配 - this.yearMatchers.add(new AlwaysTrueValueMatcher()); - } - matcherSize++; - } - // -------------------------------------------------------------------------------------- Private method end } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java index 4ffff81e1..a95fb3b67 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java @@ -1,5 +1,7 @@ package cn.hutool.cron.pattern.matcher; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import java.util.Collections; @@ -7,28 +9,50 @@ import java.util.List; /** * 将表达式中的数字值列表转换为Boolean数组,匹配时匹配相应数组位 - * @author Looly * + * @author Looly */ -public class BoolArrayValueMatcher implements ValueMatcher{ - +public class BoolArrayValueMatcher implements ValueMatcher { + + /** + * 用户定义此字段的最小值 + */ + private final int minValue; private final boolean[] bValues; - + + /** + * 构造 + * + * @param intValueList 匹配值列表 + */ public BoolArrayValueMatcher(List intValueList) { + Assert.isTrue(CollUtil.isNotEmpty(intValueList), "Values must be not empty!"); bValues = new boolean[Collections.max(intValueList) + 1]; + int min = Integer.MAX_VALUE; for (Integer value : intValueList) { + min = Math.min(min, value); bValues[value] = true; } + this.minValue = min; } @Override public boolean match(Integer value) { - if(null == value || value >= bValues.length){ + if (null == value || value >= bValues.length) { return false; } return bValues[value]; } - + + /** + * 获取表达式定义的最小值 + * + * @return 最小值 + */ + public int getMinValue() { + return this.minValue; + } + @Override public String toString() { return StrUtil.format("Matcher:{}", new Object[]{this.bValues}); diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/MatcherTable.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/MatcherTable.java new file mode 100644 index 000000000..9dc20ff02 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/MatcherTable.java @@ -0,0 +1,119 @@ +package cn.hutool.cron.pattern.matcher; + +import java.time.Year; +import java.util.ArrayList; +import java.util.List; + +/** + * 时间匹配表,用于存放定时任务表达式解析后的结构信息 + * + * @author looly + * @since 5.8.0 + */ +public class MatcherTable { + + /** + * 匹配器个数,取决于复合任务表达式中的单一表达式个数 + */ + public int matcherSize; + /** + * 秒字段匹配列表 + */ + public final List secondMatchers; + /** + * 分字段匹配列表 + */ + public final List minuteMatchers; + /** + * 时字段匹配列表 + */ + public final List hourMatchers; + /** + * 每月几号字段匹配列表 + */ + public final List dayOfMonthMatchers; + /** + * 月字段匹配列表 + */ + public final List monthMatchers; + /** + * 星期字段匹配列表 + */ + public final List dayOfWeekMatchers; + /** + * 年字段匹配列表 + */ + public final List yearMatchers; + + /** + * 构造 + * + * @param size 表达式个数,用于表示复合表达式中单个表达式个数 + */ + public MatcherTable(int size) { + matcherSize = size; + secondMatchers = new ArrayList<>(size); + minuteMatchers = new ArrayList<>(size); + hourMatchers = new ArrayList<>(size); + dayOfMonthMatchers = new ArrayList<>(size); + monthMatchers = new ArrayList<>(size); + dayOfWeekMatchers = new ArrayList<>(size); + yearMatchers = new ArrayList<>(size); + } + + /** + * 给定时间是否匹配定时任务表达式 + * + * @param second 秒数,-1表示不匹配此项 + * @param minute 分钟 + * @param hour 小时 + * @param dayOfMonth 天 + * @param month 月 + * @param dayOfWeek 周几 + * @param year 年 + * @return 如果匹配返回 {@code true}, 否则返回 {@code false} + */ + public boolean match(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) { + for (int i = 0; i < matcherSize; i++) { + boolean eval = ((second < 0) || secondMatchers.get(i).match(second)) // 匹配秒(非秒匹配模式下始终返回true) + && minuteMatchers.get(i).match(minute)// 匹配分 + && hourMatchers.get(i).match(hour)// 匹配时 + && isMatchDayOfMonth(dayOfMonthMatchers.get(i), dayOfMonth, month, Year.isLeap(year))// 匹配日 + && monthMatchers.get(i).match(month) // 匹配月 + && dayOfWeekMatchers.get(i).match(dayOfWeek)// 匹配周 + && isMatch(yearMatchers, i, year);// 匹配年 + if (eval) { + return true; + } + } + return false; + } + + /** + * 是否匹配日(指定月份的第几天) + * + * @param matcher {@link ValueMatcher} + * @param dayOfMonth 日 + * @param month 月 + * @param isLeapYear 是否闰年 + * @return 是否匹配 + */ + private static boolean isMatchDayOfMonth(ValueMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) { + return ((matcher instanceof DayOfMonthValueMatcher) // + ? ((DayOfMonthValueMatcher) matcher).match(dayOfMonth, month, isLeapYear) // + : matcher.match(dayOfMonth)); + } + + /** + * 是否匹配指定的日期时间位置 + * + * @param matchers 匹配器列表 + * @param index 位置 + * @param value 被匹配的值 + * @return 是否匹配 + * @since 4.0.2 + */ + private static boolean isMatch(List matchers, int index, int value) { + return (matchers.size() <= index) || matchers.get(index).match(value); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/AbsValueParser.java similarity index 58% rename from hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java rename to hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/AbsValueParser.java index 4cc06ccb0..a436e78de 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/AbsValueParser.java @@ -1,53 +1,111 @@ -package cn.hutool.cron.pattern.matcher; +package cn.hutool.cron.pattern.parser; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.cron.CronException; -import cn.hutool.cron.pattern.parser.DayOfMonthValueParser; -import cn.hutool.cron.pattern.parser.ValueParser; -import cn.hutool.cron.pattern.parser.YearValueParser; +import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher; +import cn.hutool.cron.pattern.matcher.BoolArrayValueMatcher; +import cn.hutool.cron.pattern.matcher.ValueMatcher; import java.util.ArrayList; import java.util.List; /** - * {@link ValueMatcher} 构建器,用于构建表达式中每一项的匹配器 - * @author Looly + * 简易值转换器。将给定String值转为int,并限定最大值和最小值
      + * 此类同时识别{@code L} 为最大值。 * + * @author Looly */ -public class ValueMatcherBuilder { - +public abstract class AbsValueParser implements ValueParser { + + /** + * 最小值(包括) + */ + protected int min; + /** + * 最大值(包括) + */ + protected int max; + + /** + * 构造 + * + * @param min 最小值(包括) + * @param max 最大值(包括) + */ + public AbsValueParser(int min, int max) { + if (min > max) { + this.min = max; + this.max = min; + } else { + this.min = min; + this.max = max; + } + } + + @Override + public int parse(String value) throws CronException { + if ("L".equalsIgnoreCase(value)) { + // L表示最大值 + return max; + } + + int i; + try { + i = Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new CronException(e, "Invalid integer value: '{}'", value); + } + if (i < min || i > max) { + throw new CronException("Value {} out of range: [{} , {}]", i, min, max); + } + return i; + } + + @Override + public int getMin() { + return this.min; + } + + @Override + public int getMax() { + return this.max; + } + /** * 处理定时任务表达式每个时间字段
      * 多个时间使用逗号分隔 - * + * * @param value 某个时间字段 - * @param parser 针对这个时间字段的解析器 * @return List */ - public static ValueMatcher build(String value, ValueParser parser) { + @Override + public ValueMatcher parseAsValueMatcher(String value) { if (isMatchAllStr(value)) { //兼容Quartz的"?"表达式,不会出现互斥情况,与"*"作用相同 return new AlwaysTrueValueMatcher(); } - List values = parseArray(value, parser); + List values = parseArray(value); if (values.size() == 0) { throw new CronException("Invalid field: [{}]", value); } - if (parser instanceof DayOfMonthValueParser) { - //考虑每月的天数不同,且存在闰年情况,日匹配单独使用 - return new DayOfMonthValueMatcher(values); - }else if(parser instanceof YearValueParser){ - //考虑年数字太大,不适合boolean数组,单独使用列表遍历匹配 - return new YearValueMatcher(values); - }else { - return new BoolArrayValueMatcher(values); - } + return buildValueMatcher(values); } - + + /** + * 根据解析的数字值列表构建{@link ValueMatcher}
      + * 默认为{@link BoolArrayValueMatcher},如果有特殊实现,子类须重写此方法 + * + * @param values 数字值列表 + * @return {@link ValueMatcher} + */ + protected ValueMatcher buildValueMatcher(List values){ + return new BoolArrayValueMatcher(values); + } + /** * 处理数组形式表达式
      * 处理的形式包括: @@ -56,15 +114,14 @@ public class ValueMatcherBuilder { *
    • a,b,c,d
    • *
    * @param value 子表达式值 - * @param parser 针对这个字段的解析器 * @return 值列表 */ - private static List parseArray(String value, ValueParser parser){ + private List parseArray(String value){ final List values = new ArrayList<>(); - + final List parts = StrUtil.split(value, StrUtil.C_COMMA); for (String part : parts) { - CollUtil.addAllIfNotContains(values, parseStep(part, parser)); + CollUtil.addAllIfNotContains(values, parseStep(part)); } return values; } @@ -77,24 +134,23 @@ public class ValueMatcherBuilder { *
  • a/b*/b
  • *
  • a-b/2
  • * - * + * * @param value 表达式值 - * @param parser 针对这个时间字段的解析器 * @return List */ - private static List parseStep(String value, ValueParser parser) { + private List parseStep(String value) { final List parts = StrUtil.split(value, StrUtil.C_SLASH); int size = parts.size(); - + List results; if (size == 1) {// 普通形式 - results = parseRange(value, -1, parser); + results = parseRange(value, -1); } else if (size == 2) {// 间隔形式 - final int step = parser.parse(parts.get(1)); + final int step = parse(parts.get(1)); if (step < 1) { throw new CronException("Non positive divisor for field: [{}]", value); } - results = parseRange(parts.get(0), step, parser); + results = parseRange(parts.get(0), step); } else { throw new CronException("Invalid syntax of field: [{}]", value); } @@ -110,21 +166,20 @@ public class ValueMatcherBuilder { *
  • 8-3
  • *
  • 3-3
  • * - * + * * @param value 范围表达式 * @param step 步进 - * @param parser 针对这个时间字段的解析器 * @return List */ - private static List parseRange(String value, int step, ValueParser parser) { + private List parseRange(String value, int step) { final List results = new ArrayList<>(); - + // 全部匹配形式 if (value.length() <= 2) { //根据步进的第一个数字确定起始时间,类似于 12/3则从12(秒、分等)开始 - int minValue = parser.getMin(); + int minValue = getMin(); if(false == isMatchAllStr(value)) { - minValue = Math.max(minValue, parser.parse(value)); + minValue = Math.max(minValue, parse(value)); }else { //在全匹配模式下,如果步进不存在,表示步进为1 if(step < 1) { @@ -132,7 +187,7 @@ public class ValueMatcherBuilder { } } if(step > 0) { - final int maxValue = parser.getMax(); + final int maxValue = getMax(); if(minValue > maxValue) { throw new CronException("Invalid value {} > {}", minValue, maxValue); } @@ -151,15 +206,15 @@ public class ValueMatcherBuilder { List parts = StrUtil.split(value, '-'); int size = parts.size(); if (size == 1) {// 普通值 - final int v1 = parser.parse(value); + final int v1 = parse(value); if(step > 0) {//类似 20/2的形式 - NumberUtil.appendRange(v1, parser.getMax(), step, results); + NumberUtil.appendRange(v1, getMax(), step, results); }else { results.add(v1); } } else if (size == 2) {// range值 - final int v1 = parser.parse(parts.get(0)); - final int v2 = parser.parse(parts.get(1)); + final int v1 = parse(parts.get(0)); + final int v2 = parse(parts.get(1)); if(step < 1) { //在range模式下,如果步进不存在,表示步进为1 step = 1; @@ -167,21 +222,21 @@ public class ValueMatcherBuilder { if (v1 < v2) {// 正常范围,例如:2-5 NumberUtil.appendRange(v1, v2, step, results); } else if (v1 > v2) {// 逆向范围,反选模式,例如:5-2 - NumberUtil.appendRange(v1, parser.getMax(), step, results); - NumberUtil.appendRange(parser.getMin(), v2, step, results); + NumberUtil.appendRange(v1, getMax(), step, results); + NumberUtil.appendRange(getMin(), v2, step, results); } else {// v1 == v2,此时与单值模式一致 - NumberUtil.appendRange(v1, parser.getMax(), step, results); + NumberUtil.appendRange(v1, getMax(), step, results); } } else { throw new CronException("Invalid syntax of field: [{}]", value); } return results; } - + /** * 是否为全匹配符
    * 全匹配符指 * 或者 ? - * + * * @param value 被检查的值 * @return 是否为全匹配符 * @since 4.1.18 diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/CronPatternParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/CronPatternParser.java new file mode 100644 index 000000000..052fb42cf --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/CronPatternParser.java @@ -0,0 +1,116 @@ +package cn.hutool.cron.pattern.parser; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher; +import cn.hutool.cron.pattern.matcher.MatcherTable; + +import java.util.List; + +/** + * 定时任务表达式解析器,用于将表达式字符串解析为{@link MatcherTable} + * + * @author looly + * @since 5.8.0 + */ +public class CronPatternParser { + + private static final ValueParser SECOND_VALUE_PARSER = new SecondValueParser(); + private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser(); + private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser(); + private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser(); + private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser(); + private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser(); + private static final ValueParser YEAR_VALUE_PARSER = new YearValueParser(); + + /** + * 创建表达式解析器 + * + * @return CronPatternParser + */ + public static CronPatternParser create() { + return new CronPatternParser(); + } + + private MatcherTable matcherTable; + + /** + * 解析表达式到匹配表中 + * + * @param cronPattern 复合表达式 + * @return {@link MatcherTable} + */ + public MatcherTable parse(String cronPattern) { + parseGroupPattern(cronPattern); + return this.matcherTable; + } + + /** + * 解析复合任务表达式,格式为: + *
    +	 *     cronA | cronB | ...
    +	 * 
    + * + * @param groupPattern 复合表达式 + */ + private void parseGroupPattern(String groupPattern) { + final List patternList = StrUtil.split(groupPattern, '|'); + matcherTable = new MatcherTable(patternList.size()); + for (String pattern : patternList) { + parseSinglePattern(pattern); + } + } + + /** + * 解析单一定时任务表达式 + * + * @param pattern 表达式 + */ + private void parseSinglePattern(String pattern) { + final String[] parts = pattern.split("\\s"); + + int offset = 0;// 偏移量用于兼容Quartz表达式,当表达式有6或7项时,第一项为秒 + if (parts.length == 6 || parts.length == 7) { + offset = 1; + } else if (parts.length != 5) { + throw new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern); + } + + // 秒,如果不支持秒的表达式,则第一位按照表达式生成时间的秒数赋值,表示整分匹配 + final String secondPart = (1 == offset) ? parts[0] : String.valueOf(DateUtil.date().second()); + parseToTable(SECOND_VALUE_PARSER, secondPart); + + // 分 + parseToTable(MINUTE_VALUE_PARSER, parts[offset]); + + // 时 + parseToTable(HOUR_VALUE_PARSER, parts[1 + offset]); + + // 天 + parseToTable(DAY_OF_MONTH_VALUE_PARSER, parts[2 + offset]); + + // 月 + parseToTable(MONTH_VALUE_PARSER, parts[3 + offset]); + + // 周 + parseToTable(DAY_OF_WEEK_VALUE_PARSER, parts[4 + offset]); + + // 年 + if (parts.length == 7) {// 支持年的表达式 + parseToTable(YEAR_VALUE_PARSER, parts[6]); + } else {// 不支持年的表达式,全部匹配 + matcherTable.yearMatchers.add(new AlwaysTrueValueMatcher()); + } + } + + /** + * 将表达式解析后加入到{@link #matcherTable}中 + * + * @param valueParser 表达式解析器 + * @param patternPart 表达式部分 + */ + private void parseToTable(ValueParser valueParser, String patternPart) { + valueParser.parseTo(this.matcherTable, patternPart); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java index 2cac2a8b5..e3ddea83e 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java @@ -1,16 +1,24 @@ package cn.hutool.cron.pattern.parser; import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.DayOfMonthValueMatcher; +import cn.hutool.cron.pattern.matcher.MatcherTable; +import cn.hutool.cron.pattern.matcher.ValueMatcher; + +import java.util.List; /** * 每月的几号值处理
    * 每月最多31天,32和“L”都表示最后一天 - * + * * @author Looly * */ -public class DayOfMonthValueParser extends SimpleValueParser { +public class DayOfMonthValueParser extends AbsValueParser { + /** + * 构造 + */ public DayOfMonthValueParser() { super(1, 31); } @@ -23,4 +31,19 @@ public class DayOfMonthValueParser extends SimpleValueParser { return super.parse(value); } } + + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.dayOfMonthMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'day of month' field error!", pattern); + } + } + + @Override + protected ValueMatcher buildValueMatcher(List values) { + //考虑每月的天数不同,且存在闰年情况,日匹配单独使用 + return new DayOfMonthValueMatcher(values); + } } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java index bad5fa9fc..8b608db44 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java @@ -1,23 +1,26 @@ package cn.hutool.cron.pattern.parser; import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.MatcherTable; /** * 星期值处理
    - * 1表示星期一,2表示星期二,依次类推,0和7都可以表示星期日 - * - * @author Looly + * 1表示星期一,2表示星期二,依次类推,0和7都可以表示星期日
    + * {@code L}表示周六 * + * @author Looly */ -public class DayOfWeekValueParser extends SimpleValueParser { - - /** Weeks aliases. */ - private static final String[] ALIASES = { "sun", "mon", "tue", "wed", "thu", "fri", "sat" }; +public class DayOfWeekValueParser extends AbsValueParser { + + /** + * Weeks aliases. + */ + private static final String[] ALIASES = {"sun", "mon", "tue", "wed", "thu", "fri", "sat"}; public DayOfWeekValueParser() { super(0, 7); } - + /** * 对于星期提供转换
    * 1表示星期一,2表示星期二,依次类推,0和7都可以表示星期日 @@ -31,18 +34,28 @@ public class DayOfWeekValueParser extends SimpleValueParser { } } + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.dayOfWeekMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'day of week' field error!", pattern); + } + } + /** * 解析别名 + * * @param value 别名值 * @return 月份int值 * @throws CronException 无效别名抛出此异常 */ private int parseAlias(String value) throws CronException { - if("L".equalsIgnoreCase(value)){ + if ("L".equalsIgnoreCase(value)) { //最后一天为星期六 return ALIASES.length - 1; } - + for (int i = 0; i < ALIASES.length; i++) { if (ALIASES[i].equalsIgnoreCase(value)) { return i; diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java index 5041f3837..bc7b5159c 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java @@ -1,14 +1,26 @@ package cn.hutool.cron.pattern.parser; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.MatcherTable; + /** - * 小时值处理 - * @author Looly + * 小时值处理
    + * 小时被限定在0-23 * + * @author Looly */ -public class HourValueParser extends SimpleValueParser{ - +public class HourValueParser extends AbsValueParser { + public HourValueParser() { super(0, 23); } + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.hourMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'hour' field error!", pattern); + } + } } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java index 474660477..f2f76768a 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java @@ -1,14 +1,29 @@ package cn.hutool.cron.pattern.parser; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.MatcherTable; + /** - * 分钟值处理 - * @author Looly + * 分钟值处理
    + * 限定于0-59 * + * @author Looly */ -public class MinuteValueParser extends SimpleValueParser{ - +public class MinuteValueParser extends AbsValueParser { + + /** + * 构造 + */ public MinuteValueParser() { super(0, 59); } + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.minuteMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'minute' field error!", pattern); + } + } } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java index 2f2f47d52..e58a62bb9 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java @@ -1,22 +1,25 @@ package cn.hutool.cron.pattern.parser; import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.MatcherTable; /** - * 月份值处理 - * - * @author Looly + * 月份值处理
    + * 限定于1-12,1表示一月,支持别名(忽略大小写),如一月是{@code jan} * + * @author Looly */ -public class MonthValueParser extends SimpleValueParser { +public class MonthValueParser extends AbsValueParser { - /** Months aliases. */ - private static final String[] ALIASES = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" }; + /** + * Months aliases. + */ + private static final String[] ALIASES = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}; public MonthValueParser() { super(1, 12); } - + @Override public int parse(String value) throws CronException { try { @@ -26,8 +29,18 @@ public class MonthValueParser extends SimpleValueParser { } } + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.monthMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'month' field error!", pattern); + } + } + /** * 解析别名 + * * @param value 别名值 * @return 月份int值 * @throws CronException 无效月别名抛出此异常 diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java index b3697ccbc..084157a81 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java @@ -1,9 +1,22 @@ package cn.hutool.cron.pattern.parser; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.MatcherTable; + /** - * 秒值处理 - * @author Looly + * 秒值处理
    + * 限定于0-59 * + * @author Looly */ -public class SecondValueParser extends MinuteValueParser{ +public class SecondValueParser extends MinuteValueParser { + + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.secondMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'second' field error!", pattern); + } + } } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java deleted file mode 100644 index 280e92290..000000000 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java +++ /dev/null @@ -1,61 +0,0 @@ -package cn.hutool.cron.pattern.parser; - -import cn.hutool.cron.CronException; - -/** - * 简易值转换器。将给定String值转为int - * @author Looly - * - */ -public class SimpleValueParser implements ValueParser { - - /** 最小值(包括) */ - protected int min; - /** 最大值(包括) */ - protected int max; - - /** - * 构造 - * - * @param min 最小值(包括) - * @param max 最大值(包括) - */ - public SimpleValueParser(int min, int max) { - if(min > max){ - this.min = max; - this.max = min; - }else{ - this.min = min; - this.max = max; - } - } - - @Override - public int parse(String value) throws CronException { - if("L".equalsIgnoreCase(value)){ - // L表示最大值 - return max; - } - - int i; - try { - i = Integer.parseInt(value); - } catch (NumberFormatException e) { - throw new CronException(e, "Invalid integer value: '{}'", value); - } - if (i < min || i > max) { - throw new CronException("Value {} out of range: [{} , {}]", i, min, max); - } - return i; - } - - @Override - public int getMin() { - return this.min; - } - - @Override - public int getMax() { - return this.max; - } -} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java index 97c62fbf1..2e49f624f 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java @@ -1,5 +1,8 @@ package cn.hutool.cron.pattern.parser; +import cn.hutool.cron.pattern.matcher.MatcherTable; +import cn.hutool.cron.pattern.matcher.ValueMatcher; + /** * 值处理接口
    * 值处理用于限定表达式中相应位置的值范围,并转换表达式值为int值 @@ -8,6 +11,28 @@ package cn.hutool.cron.pattern.parser; */ public interface ValueParser { + /** + * 解析表达式后,加入到{@link MatcherTable}的对应列表中 + * + * @param matcherTable {@link MatcherTable} + * @param pattern 对应时间部分的表达式 + */ + void parseTo(MatcherTable matcherTable, String pattern); + + /** + * 解析表达式对应部分为{@link ValueMatcher},支持的表达式包括: + *
      + *
    • 单值或通配符形式,如 a*
    • + *
    • 数组形式,如 1,2,3
    • + *
    • 间隔形式,如 a/b*/b
    • + *
    • 范围形式,如 3-8
    • + *
    + * + * @param pattern 对应时间部分的表达式 + * @return {@link ValueMatcher} + */ + ValueMatcher parseAsValueMatcher(String pattern); + /** * 处理String值并转为int
    * 转换包括: @@ -15,7 +40,7 @@ public interface ValueParser { *
  • 数字字符串转为数字
  • *
  • 别名转为对应的数字(如月份和星期)
  • * - * + * * @param value String值 * @return int */ @@ -23,14 +48,14 @@ public interface ValueParser { /** * 返回最小值 - * + * * @return 最小值 */ int getMin(); /** * 返回最大值 - * + * * @return 最大值 */ int getMax(); diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java index 13222c3e6..484a1cf4b 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java @@ -1,14 +1,36 @@ package cn.hutool.cron.pattern.parser; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.MatcherTable; +import cn.hutool.cron.pattern.matcher.ValueMatcher; +import cn.hutool.cron.pattern.matcher.YearValueMatcher; + +import java.util.List; + /** - * 年值处理 - * @author Looly + * 年值处理
    + * 年的限定在1970-2099年 * + * @author Looly */ -public class YearValueParser extends SimpleValueParser{ - +public class YearValueParser extends AbsValueParser { + public YearValueParser() { super(1970, 2099); } + @Override + public void parseTo(MatcherTable matcherTable, String pattern) { + try { + matcherTable.yearMatchers.add(parseAsValueMatcher(pattern)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'year' field error!", pattern); + } + } + + @Override + protected ValueMatcher buildValueMatcher(List values) { + //考虑年数字太大,不适合boolean数组,单独使用列表遍历匹配 + return new YearValueMatcher(values); + } } diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java index e6c916b6c..7625b9444 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java @@ -163,7 +163,8 @@ public class HMac implements Serializable { * @return 摘要 */ public String digestBase64(String data, Charset charset, boolean isUrlSafe) { - return Base64.encodeStr(digest(data, charset), false, isUrlSafe); + final byte[] digest = digest(data, charset); + return isUrlSafe ? Base64.encodeUrlSafe(digest) : Base64.encode(digest); } /** diff --git a/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java b/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java index 44c12c40a..3dabe6288 100644 --- a/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java +++ b/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java @@ -23,7 +23,7 @@ public enum GlobalHeaders { /** * 存储头信息 */ - Map> headers = new HashMap<>(); + final Map> headers = new HashMap<>(); /** * 构造 @@ -43,6 +43,10 @@ public enum GlobalHeaders { // https://stackoverflow.com/questions/9096987/how-to-overwrite-http-header-host-in-a-httpurlconnection/9098440 System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + //解决server certificate change is restricted during renegotiation问题 + System.setProperty("jdk.tls.allowUnsafeServerCertChange", "true"); + System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true"); + if (isReset) { this.headers.clear(); } diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java index 17860c8ae..8db7e1ced 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java @@ -345,7 +345,7 @@ public class ExcelUtil { * @return {@link BigExcelWriter} * @since 4.1.13 */ - public static ExcelWriter getBigWriter() { + public static BigExcelWriter getBigWriter() { try { return new BigExcelWriter(); } catch (NoClassDefFoundError e) { @@ -362,7 +362,7 @@ public class ExcelUtil { * @return {@link BigExcelWriter} * @since 4.1.13 */ - public static ExcelWriter getBigWriter(int rowAccessWindowSize) { + public static BigExcelWriter getBigWriter(int rowAccessWindowSize) { try { return new BigExcelWriter(rowAccessWindowSize); } catch (NoClassDefFoundError e) { diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java index b50034cb3..2c4ec4b71 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java @@ -3,6 +3,7 @@ package cn.hutool.poi.excel.sax; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.poi.excel.sax.handler.RowHandler; import cn.hutool.poi.exceptions.POIException; @@ -127,13 +128,9 @@ public class Excel07SaxReader implements ExcelSaxReader { } // 获取共享字符串表 - try { - this.handler.sharedStrings = xssfReader.getSharedStringsTable(); - } catch (IOException e) { - throw new IORuntimeException(e); - } catch (InvalidFormatException e) { - throw new POIException(e); - } + // POI-5.2.0开始返回值有所变更,导致实际使用时提示方法未找到,此处使用反射调用,解决不同版本返回值变更问题 + //this.handler.sharedStrings = xssfReader.getSharedStringsTable(); + this.handler.sharedStrings = ReflectUtil.invoke(xssfReader, "getSharedStringsTable"); return readSheets(xssfReader, idOrRidOrSheetName); }